@latitude-data/openclaw-telemetry 0.0.8 → 0.0.9
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/README.md +23 -3
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +61 -4
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ OpenClaw plugin that streams every agent run to [Latitude](https://latitude.so)
|
|
|
14
14
|
The companion CLI handles every step (install, config, validate, restart) in one command:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx -y @latitude-data/openclaw-telemetry-cli@0.0.
|
|
17
|
+
npx -y @latitude-data/openclaw-telemetry-cli@0.0.9 install
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
It prompts for your API key and project slug, runs `openclaw plugins install` for you, writes the plugin entry into `openclaw.json`, adds the plugin to `plugins.allow`, validates the result, and (on TTY) offers to restart the gateway. See the [CLI README](https://github.com/latitude-dev/latitude-llm/tree/main/packages/telemetry/openclaw-cli#readme) for the full flag matrix, dry-run mode, custom config dir, and CI usage.
|
|
@@ -26,7 +26,7 @@ If you'd rather not use the CLI, do exactly what it does, in four steps:
|
|
|
26
26
|
#### 1. Install the runtime
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
openclaw plugins install @latitude-data/openclaw-telemetry@0.0.
|
|
29
|
+
openclaw plugins install @latitude-data/openclaw-telemetry@0.0.9
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Pin to an exact version. OpenClaw's `security audit --deep` warns about unpinned install specs, so always include the `@<version>` suffix.
|
|
@@ -108,7 +108,7 @@ Merge with whatever else is in `openclaw.json`. Then run `openclaw config valida
|
|
|
108
108
|
If you installed via the CLI:
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
|
-
npx -y @latitude-data/openclaw-telemetry-cli@0.0.
|
|
111
|
+
npx -y @latitude-data/openclaw-telemetry-cli@0.0.9 uninstall
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
Manual uninstall:
|
|
@@ -211,6 +211,7 @@ Two blocks live under `plugins.entries["@latitude-data/openclaw-telemetry"]`:
|
|
|
211
211
|
| `project` | yes | — | Slug of the project to route traces into. |
|
|
212
212
|
| `baseUrl` | no | `https://ingest.latitude.so` | Override OTLP ingest origin. The CLI sets this only when `--staging` or `--dev` is passed. |
|
|
213
213
|
| `allowConversationAccess` | no | `false` | When `true`, attach raw prompts, assistant responses, system instructions, and tool I/O to spans. When `false`, emit only timing, token usage, model name, agent id, and structural ids — same span tree, scrubbed payloads. **Must match `hooks.allowConversationAccess` below — see [The two flags](#the-two-flags).** |
|
|
214
|
+
| `redact` | no | — | Custom local attribute redaction before export: `{ "attributes": ["/^gen_ai\\.(input|output)\\.messages$/"], "mask": "[]" }`. Patterns are exact strings, regex source strings, or `/pattern/flags` strings. |
|
|
214
215
|
| `enabled` | no | `true` | Set to `false` to pause emission without uninstalling. |
|
|
215
216
|
| `debug` | no | `false` | Log diagnostic lines to stderr (visible in the gateway log). |
|
|
216
217
|
|
|
@@ -245,6 +246,25 @@ For hand-edited configs, leaving `allowConversationAccess` out entirely produces
|
|
|
245
246
|
|
|
246
247
|
To pause emission without uninstalling, set `enabled: false` on the plugin entry, or `LATITUDE_OPENCLAW_ENABLED=0` in the gateway environment.
|
|
247
248
|
|
|
249
|
+
For field-level PII controls while keeping content capture enabled, add `config.redact`. For example, to send empty message arrays for prompts/responses before anything leaves the gateway:
|
|
250
|
+
|
|
251
|
+
```jsonc
|
|
252
|
+
{
|
|
253
|
+
"plugins": {
|
|
254
|
+
"entries": {
|
|
255
|
+
"@latitude-data/openclaw-telemetry": {
|
|
256
|
+
"config": {
|
|
257
|
+
"redact": {
|
|
258
|
+
"attributes": ["/^gen_ai\\.(input|output)\\.messages$/"],
|
|
259
|
+
"mask": "[]"
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
248
268
|
## Supported OpenClaw versions
|
|
249
269
|
|
|
250
270
|
Requires **2026.4.25 or newer**. Earlier versions either reject `hooks.allowConversationAccess` outright (≤ 2026.4.21) or have unverified dispatch gating (2026.4.22 – 2026.4.24). The CLI's version check aborts on older versions; manual installs run into validation errors. Run `npm install -g openclaw@latest` to upgrade.
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
//#region src/redaction.d.ts
|
|
2
|
+
interface RedactConfig {
|
|
3
|
+
attributes: string[];
|
|
4
|
+
mask: string;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
1
7
|
//#region src/config.d.ts
|
|
2
8
|
interface Config {
|
|
3
9
|
apiKey: string;
|
|
@@ -12,6 +18,7 @@ interface Config {
|
|
|
12
18
|
* names, agent ids, and timings are unaffected.
|
|
13
19
|
*/
|
|
14
20
|
allowConversationAccess: boolean;
|
|
21
|
+
redact?: RedactConfig | undefined;
|
|
15
22
|
}
|
|
16
23
|
//#endregion
|
|
17
24
|
//#region src/logger.d.ts
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/config.ts","../src/logger.ts","../src/span-builder.ts","../src/plugin.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/redaction.ts","../src/config.ts","../src/logger.ts","../src/span-builder.ts","../src/plugin.ts"],"mappings":";UAEiB,YAAA;EACf,UAAA;EACA,IAAA;AAAA;;;UCFe,MAAA;EACf,MAAA;EACA,OAAA;EACA,OAAA;EACA,OAAA;EACA,KAAA;;;AALF;;;;EAYE,uBAAA;EACA,MAAA,GAAS,YAAA;AAAA;;;UCbM,MAAA;EACf,KAAA,GAAQ,GAAA;EACR,IAAA,GAAO,GAAA;AAAA;;;AFFT;;;;;;;;ACAA;;;;;;;;;;;;;;;;;ACAA;AFAA,UGoDiB,UAAA;;EAEf,MAAA;EDrDA;ECuDA,OAAA;EDtDA;ECwDA,YAAA;EDxDkB;EC0DlB,IAAA;EACA,OAAA;EACA,KAAA;;EAEA,KAAA,EAAO,MAAA,SAAe,SAAA;EAZG;EAczB,OAAA;EACA,YAAA;AAAA;AAAA,KAGU,SAAA,2CAAoD,MAAA;AAAA,UAoD/C,WAAA;EA9Df;EAgEA,KAAA;EA9DA;EAgEA,KAAA,EAAO,UAAA;AAAA;;;;;;;;;AF9HT;;;;;UG8BiB,qBAAA;EACf,MAAA,GAAS,MAAA;EACT,YAAA,GAAe,MAAA;EACf,EAAA,qBACE,QAAA,EAAU,CAAA,EACV,OAAA,GAAU,KAAA,WAAgB,GAAA,uBAC1B,IAAA;IAAS,QAAA;EAAA;AAAA;AAAA,UAII,eAAA;EH3BM;EG6BrB,MAAA,GAAS,MAAA;;EAET,MAAA,GAAS,MAAA;EF5CM;;;;EEiDf,MAAA,IAAU,MAAA,EAAQ,WAAA;AAAA;;;;;;;;ADGpB;;;;;iBCYwB,sBAAA,CAAuB,GAAA,EAAK,qBAAA,EAAuB,IAAA,GAAM,eAAA"}
|
package/dist/plugin.js
CHANGED
|
@@ -29,6 +29,57 @@ async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
|
+
//#region src/redaction.ts
|
|
33
|
+
function parseRedactConfig(value) {
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
35
|
+
const obj = value;
|
|
36
|
+
const attributes = parseAttributes(obj.attributes);
|
|
37
|
+
if (attributes.length === 0) return void 0;
|
|
38
|
+
return {
|
|
39
|
+
attributes,
|
|
40
|
+
mask: typeof obj.mask === "string" ? obj.mask : "******"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function redactAttributes(attributes, config) {
|
|
44
|
+
if (!config) return attributes;
|
|
45
|
+
const matchers = config.attributes.map(toMatcher).filter((matcher) => !!matcher);
|
|
46
|
+
if (matchers.length === 0) return attributes;
|
|
47
|
+
return attributes.map((attr) => matchers.some((matches) => matches(attr.key)) ? redactedAttr(attr.key, config.mask) : attr);
|
|
48
|
+
}
|
|
49
|
+
function parseAttributes(value) {
|
|
50
|
+
if (!Array.isArray(value)) return [];
|
|
51
|
+
return value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
52
|
+
}
|
|
53
|
+
function toMatcher(pattern) {
|
|
54
|
+
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
|
55
|
+
const end = pattern.lastIndexOf("/");
|
|
56
|
+
try {
|
|
57
|
+
const regex = new RegExp(pattern.slice(1, end), pattern.slice(end + 1));
|
|
58
|
+
return (key) => {
|
|
59
|
+
regex.lastIndex = 0;
|
|
60
|
+
return regex.test(key);
|
|
61
|
+
};
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const regex = new RegExp(pattern);
|
|
68
|
+
return (key) => {
|
|
69
|
+
regex.lastIndex = 0;
|
|
70
|
+
return key === pattern || regex.test(key);
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return (key) => key === pattern;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function redactedAttr(key, mask) {
|
|
77
|
+
return {
|
|
78
|
+
key,
|
|
79
|
+
value: { stringValue: mask }
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
32
83
|
//#region src/config.ts
|
|
33
84
|
const DEFAULT_BASE_URL = "https://ingest.latitude.so";
|
|
34
85
|
/**
|
|
@@ -58,13 +109,15 @@ function loadConfig(pluginConfig = void 0) {
|
|
|
58
109
|
const debug = pickBool(fromOpts.debug) ?? false;
|
|
59
110
|
const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false;
|
|
60
111
|
const explicitlyDisabled = pickBool(fromOpts.enabled) === false;
|
|
112
|
+
const hasCreds = apiKey !== "" && project !== "";
|
|
61
113
|
return {
|
|
62
114
|
apiKey,
|
|
63
115
|
baseUrl,
|
|
64
116
|
project,
|
|
65
117
|
debug,
|
|
66
118
|
allowConversationAccess,
|
|
67
|
-
|
|
119
|
+
redact: parseRedactConfig(fromOpts.redact),
|
|
120
|
+
enabled: hasCreds && !explicitlyDisabled
|
|
68
121
|
};
|
|
69
122
|
}
|
|
70
123
|
function pickString(value) {
|
|
@@ -85,7 +138,7 @@ function createLogger(debugEnabled) {
|
|
|
85
138
|
//#endregion
|
|
86
139
|
//#region src/otlp.ts
|
|
87
140
|
const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
|
|
88
|
-
const SCOPE_VERSION = "0.0.
|
|
141
|
+
const SCOPE_VERSION = "0.0.9";
|
|
89
142
|
/** Build an OTLP export request for a single completed agent run. */
|
|
90
143
|
function buildOtlpRequest(result, options) {
|
|
91
144
|
const spans = result.spans.map((span) => toOtlpSpan(span, options));
|
|
@@ -113,6 +166,7 @@ function toOtlpSpan(span, options) {
|
|
|
113
166
|
}
|
|
114
167
|
attrs.push(bool("latitude.captured.content", options.allowConversationAccess));
|
|
115
168
|
if (span.endMs !== void 0) attrs.push(int("openclaw.duration_ms.computed", Math.max(0, span.endMs - span.startMs)));
|
|
169
|
+
const redactedAttrs = redactAttributes(attrs, options.redact);
|
|
116
170
|
const statusCode = span.outcome === "error" ? 2 : 1;
|
|
117
171
|
return {
|
|
118
172
|
traceId: span.traceId,
|
|
@@ -122,7 +176,7 @@ function toOtlpSpan(span, options) {
|
|
|
122
176
|
kind: 1,
|
|
123
177
|
startTimeUnixNano: startNs,
|
|
124
178
|
endTimeUnixNano: endNs,
|
|
125
|
-
attributes:
|
|
179
|
+
attributes: redactedAttrs,
|
|
126
180
|
status: { code: statusCode }
|
|
127
181
|
};
|
|
128
182
|
}
|
|
@@ -957,7 +1011,10 @@ function registerLatitudePlugin(api, opts = {}) {
|
|
|
957
1011
|
return;
|
|
958
1012
|
}
|
|
959
1013
|
opts.onEmit?.(result);
|
|
960
|
-
const payload = buildOtlpRequest(result, {
|
|
1014
|
+
const payload = buildOtlpRequest(result, {
|
|
1015
|
+
allowConversationAccess: config.allowConversationAccess,
|
|
1016
|
+
redact: config.redact
|
|
1017
|
+
});
|
|
961
1018
|
postTraces({
|
|
962
1019
|
baseUrl: config.baseUrl,
|
|
963
1020
|
apiKey: config.apiKey,
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["safeJson"],"sources":["../src/client.ts","../src/config.ts","../src/logger.ts","../src/otlp.ts","../src/messages.ts","../src/span-builder.ts","../src/plugin.ts"],"sourcesContent":["import type { Logger } from \"./logger.ts\"\nimport type { OtlpExportRequest } from \"./types.ts\"\n\nexport async function postTraces({\n baseUrl,\n apiKey,\n project,\n payload,\n logger,\n timeoutMs = 10_000,\n}: {\n baseUrl: string\n apiKey: string\n project: string\n payload: OtlpExportRequest\n logger: Logger\n timeoutMs?: number\n}): Promise<void> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/v1/traces`\n const bodyText = JSON.stringify(payload)\n logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`)\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"X-Latitude-Project\": project,\n },\n body: bodyText,\n signal: controller.signal,\n })\n if (!res.ok) {\n const text = await res.text().catch(() => \"\")\n logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`)\n } else {\n logger.debug(`ingest HTTP ${res.status}`)\n }\n } catch (err) {\n logger.warn(`ingest failed: ${String(err)}`)\n } finally {\n clearTimeout(timer)\n }\n}\n","export interface Config {\n apiKey: string\n baseUrl: string\n project: string\n enabled: boolean\n debug: boolean\n /**\n * When false, the plugin still emits one span per LLM call / tool / run, but\n * scrubs raw conversation content (input/output messages, system prompt,\n * tool args, tool results, the surfaced first-prompt). Token counts, model\n * names, agent ids, and timings are unaffected.\n */\n allowConversationAccess: boolean\n}\n\nconst DEFAULT_BASE_URL = \"https://ingest.latitude.so\"\n\n/**\n * Build a `Config` from OpenClaw's per-plugin config bucket. The plugin SDK\n * passes `api.pluginConfig` (the user's `plugins.entries[id].config` block)\n * to the registration function — that's the only source.\n *\n * Earlier 0.0.x versions also fell back to environment variables when keys\n * were missing from pluginConfig. That fallback is gone deliberately:\n * OpenClaw 2026.4.25's `openclaw plugins install` runs a static-analysis\n * security scan that flags any runtime source combining environment-variable\n * access with a network-send call (we have `fetch(` in postTraces). With\n * the fallback our bundled runtime tripped the scanner. The installer\n * writes credentials to `plugins.entries[id].config` anyway, so the\n * fallback was polish-not-feature — its removal also gives a cleaner\n * privacy story (the runtime can't pick up credentials the operator\n * didn't put in openclaw.json).\n *\n * For dev-time testing with debug logs, set `config.debug = true` in\n * openclaw.json directly.\n */\nexport function loadConfig(pluginConfig: Record<string, unknown> | undefined = undefined): Config {\n const fromOpts = pluginConfig ?? {}\n\n const apiKey = pickString(fromOpts.apiKey) ?? \"\"\n const project = pickString(fromOpts.project) ?? \"\"\n const baseUrl = pickString(fromOpts.baseUrl) ?? DEFAULT_BASE_URL\n\n const debug = pickBool(fromOpts.debug) ?? false\n const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false\n\n const explicitlyDisabled = pickBool(fromOpts.enabled) === false\n const hasCreds = apiKey !== \"\" && project !== \"\"\n\n return {\n apiKey,\n baseUrl,\n project,\n debug,\n allowConversationAccess,\n enabled: hasCreds && !explicitlyDisabled,\n }\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined\n}\n\nfunction pickBool(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined\n}\n","const PREFIX = \"[latitude-openclaw]\"\n\nexport interface Logger {\n debug: (msg: string) => void\n warn: (msg: string) => void\n}\n\nexport function createLogger(debugEnabled: boolean): Logger {\n return {\n debug: debugEnabled ? (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`) : () => {},\n warn: (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`),\n }\n}\n","import { arch, hostname, platform, release } from \"node:os\"\nimport type { AttrValue, BuildResult, SpanRecord } from \"./span-builder.ts\"\nimport type { OtlpExportRequest, OtlpKeyValue, OtlpResourceSpans, OtlpSpan } from \"./types.ts\"\n\nconst SCOPE_NAME = \"@latitude-data/openclaw-telemetry\"\n\n/**\n * Build-time-baked package version.\n *\n * `__SCOPE_VERSION__` is replaced at bundle time by tsdown's `define` (see\n * `tsdown.config.ts`) with a string literal of `package.json`'s `version`,\n * so the released bundle ships a constant — no runtime file read.\n *\n * Earlier versions read `package.json` at runtime via `readFileSync` to keep\n * one source of truth for the version. That tripped OpenClaw 2026.4.26's\n * `plugins.code_safety` scanner with a \"potential-exfiltration: File read\n * combined with network send\" warning (we have `fetch(` in `client.ts`).\n * Build-time bake preserves the single source of truth (the build reads\n * `package.json` and inlines the value) while keeping the runtime free of\n * `node:fs`.\n *\n * The `typeof` check is a runtime fallback for environments where the\n * `define` substitution didn't run — chiefly vitest, which executes the\n * source files directly without going through the build. `typeof` of an\n * undeclared identifier returns `\"undefined\"` rather than throwing, which\n * keeps tests working.\n */\ndeclare const __SCOPE_VERSION__: string\nconst SCOPE_VERSION = typeof __SCOPE_VERSION__ === \"string\" ? __SCOPE_VERSION__ : \"0.0.0-dev\"\n\ninterface BuildOptions {\n /**\n * When false, attributes whose key ends in `:gated` are scrubbed from\n * spans before export — that's `gen_ai.input.messages`,\n * `gen_ai.output.messages`, `gen_ai.system_instructions`, `user_prompt`,\n * `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result`,\n * `before_compaction.messages`, `before_agent_start.{prompt,messages}`,\n * `agent_end.messages`, and `openclaw.error.message` (the last because\n * error strings can leak prompt/response content). Timing, token usage,\n * model name, ids, agent name, durations, byte counts, and the\n * `latitude.captured.content` boolean are always emitted.\n */\n allowConversationAccess: boolean\n}\n\n/** Build an OTLP export request for a single completed agent run. */\nexport function buildOtlpRequest(result: BuildResult, options: BuildOptions): OtlpExportRequest {\n const spans = result.spans.map((span) => toOtlpSpan(span, options))\n const rs: OtlpResourceSpans = {\n resource: { attributes: resourceAttrs() },\n scopeSpans: [{ scope: { name: SCOPE_NAME, version: SCOPE_VERSION }, spans }],\n }\n return { resourceSpans: [rs] }\n}\n\n// ─── SpanRecord → OtlpSpan ─────────────────────────────────────────────────\n\nfunction toOtlpSpan(span: SpanRecord, options: BuildOptions): OtlpSpan {\n const startNs = msToNs(span.startMs)\n const endNs = msToNs(span.endMs ?? span.startMs)\n\n const attrs: OtlpKeyValue[] = []\n for (const [rawKey, value] of Object.entries(span.attrs)) {\n if (value === undefined || value === null) continue\n const isGated = rawKey.endsWith(\":gated\")\n if (isGated && !options.allowConversationAccess) continue\n const key = isGated ? rawKey.slice(0, -\":gated\".length) : rawKey\n const kv = encodeAttr(key, value)\n if (kv !== undefined) attrs.push(kv)\n }\n\n // Always emit the gate state so operators can see it in the UI without\n // needing to grep the original config.\n attrs.push(bool(\"latitude.captured.content\", options.allowConversationAccess))\n // Mirror duration into the canonical name as well, when present.\n if (span.endMs !== undefined) {\n attrs.push(int(\"openclaw.duration_ms.computed\", Math.max(0, span.endMs - span.startMs)))\n }\n\n const statusCode = span.outcome === \"error\" ? 2 : 1\n return {\n traceId: span.traceId,\n spanId: span.spanId,\n parentSpanId: span.parentSpanId,\n name: span.name,\n // OTel SpanKind: 1 = INTERNAL. None of agent/model_call/tool_call/\n // compaction/subagent map cleanly to CLIENT/SERVER/PRODUCER/CONSUMER —\n // OpenClaw is the source-of-truth runtime for all of them.\n kind: 1,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: attrs,\n status: { code: statusCode },\n }\n}\n\nfunction encodeAttr(key: string, value: AttrValue): OtlpKeyValue | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value === \"string\") return str(key, value)\n if (typeof value === \"boolean\") return bool(key, value)\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? int(key, value) : { key, value: { doubleValue: value } }\n }\n // Arrays + objects → JSON string. The Latitude UI parses the gen_ai.* keys\n // as JSON; anything else lands as opaque string and is queryable as a\n // contains-substring filter.\n return str(key, safeJson(value))\n}\n\n// ─── Resource + helper attribute encoders ──────────────────────────────────\n\nfunction resourceAttrs(): OtlpKeyValue[] {\n return [\n str(\"service.name\", \"openclaw\"),\n str(\"service.version\", SCOPE_VERSION),\n str(\"host.name\", hostname()),\n str(\"host.arch\", arch()),\n str(\"os.type\", platform()),\n str(\"os.version\", release()),\n ]\n}\n\nfunction str(key: string, value: string): OtlpKeyValue {\n return { key, value: { stringValue: value } }\n}\n\nfunction int(key: string, value: number): OtlpKeyValue {\n return { key, value: { intValue: String(Math.trunc(value)) } }\n}\n\nfunction bool(key: string, value: boolean): OtlpKeyValue {\n return { key, value: { boolValue: value } }\n}\n\nfunction msToNs(ms: number): string {\n return (BigInt(Math.trunc(ms)) * 1_000_000n).toString()\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n","/**\n * Normalizes provider-specific message shapes (Anthropic, OpenAI, pi-ai) into\n * the parts-based GenAI format Latitude's parser expects:\n *\n * { role: \"system\" | \"user\" | \"assistant\" | \"tool\", parts: MessagePart[] }\n *\n * Downstream consumers cast `gen_ai.input.messages` and `gen_ai.output.messages`\n * to `GenAIMessage[]` and read `message.parts` directly (e.g. for search\n * indexing). Without normalisation those casts produce objects without\n * `parts`, breaking rendering.\n *\n * The shapes we need to handle:\n *\n * - Anthropic: `{role, content: string}` or `{role, content: ContentBlock[]}`\n * with blocks `{type: \"text\", text}`, `{type: \"tool_use\", id, name, input}`,\n * `{type: \"tool_result\", tool_use_id, content}`, `{type: \"image\", source}`,\n * `{type: \"thinking\", thinking}`.\n * - OpenAI: `{role, content: string}` or with `tool_calls` field.\n * - pi-ai (OpenClaw's wrapper) — superset of the above.\n * - Already-normalized parts-shape messages — passed through unchanged.\n *\n * Anything we don't recognize falls through to a JSON-stringified text part\n * so nothing is silently dropped.\n */\n\nexport interface MessagePart {\n type: string\n content?: string\n // Tool call (assistant invokes a tool)\n id?: string\n name?: string\n arguments?: unknown\n // Tool response (tool replies)\n response?: unknown\n // Image / multimodal\n modality?: string\n uri?: string\n [key: string]: unknown\n}\n\nexport type MessageRole = \"system\" | \"user\" | \"assistant\" | \"tool\"\n\nexport interface Message {\n role: MessageRole\n parts: MessagePart[]\n}\n\nconst ALLOWED_ROLES: ReadonlySet<MessageRole> = new Set([\"system\", \"user\", \"assistant\", \"tool\"])\n\n/**\n * Normalize a single message of any of the provider shapes we know about.\n * Returns `undefined` for non-objects so the caller can skip them.\n */\nexport function normalizeMessage(raw: unknown): Message | undefined {\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n const role = coerceRole(obj.role)\n\n // Pre-normalized: already has a parts array.\n if (Array.isArray(obj.parts)) {\n const parts: MessagePart[] = []\n for (const p of obj.parts) {\n if (p && typeof p === \"object\") parts.push(p as MessagePart)\n }\n return { role, parts: parts.length > 0 ? parts : [{ type: \"text\", content: safeJson(raw) }] }\n }\n\n const content = obj.content ?? obj.text ?? obj.message\n\n // OpenAI tool message: `{role: \"tool\", tool_call_id, content}` — handle\n // before the generic string-content branch so we emit a tool_call_response\n // part rather than a plain text part.\n if (role === \"tool\" && obj.tool_call_id !== undefined) {\n return {\n role,\n parts: [\n {\n type: \"tool_call_response\",\n id: typeof obj.tool_call_id === \"string\" ? obj.tool_call_id : \"\",\n response: content ?? safeJson(obj),\n },\n ],\n }\n }\n\n if (typeof content === \"string\") {\n const parts: MessagePart[] = [{ type: \"text\", content }]\n // OpenAI assistant messages may have tool_calls alongside string content.\n appendToolCalls(parts, obj.tool_calls)\n return { role, parts }\n }\n\n if (Array.isArray(content)) {\n const parts: MessagePart[] = []\n for (const block of content) {\n const part = normalizeBlock(block)\n if (part) parts.push(part)\n }\n appendToolCalls(parts, obj.tool_calls)\n if (parts.length === 0) parts.push({ type: \"text\", content: safeJson(content) })\n return { role, parts }\n }\n\n // Unknown shape — dump as JSON so nothing is silently dropped.\n return { role, parts: [{ type: \"text\", content: safeJson(raw) }] }\n}\n\n/** Normalize an array of provider messages. */\nexport function normalizeMessages(raw: unknown[]): Message[] {\n const out: Message[] = []\n for (const m of raw) {\n const norm = normalizeMessage(m)\n if (norm) out.push(norm)\n }\n return out\n}\n\n/** Build a single user message from a string prompt. */\nexport function userMessageFromPrompt(prompt: string): Message {\n return { role: \"user\", parts: [{ type: \"text\", content: prompt }] }\n}\n\n/** Build a single assistant message from `assistantTexts` + `lastAssistant` fallback. */\nexport function assistantMessageFromOutput(assistantTexts: string[], lastAssistant: unknown): Message {\n if (lastAssistant !== undefined) {\n const norm = normalizeMessage(lastAssistant)\n if (norm) return { ...norm, role: \"assistant\" }\n }\n const parts: MessagePart[] = []\n for (const text of assistantTexts) {\n if (text.length > 0) parts.push({ type: \"text\", content: text })\n }\n if (parts.length === 0) parts.push({ type: \"text\", content: \"\" })\n return { role: \"assistant\", parts }\n}\n\n/**\n * Wrap a system prompt string into the parts-array shape expected for\n * `gen_ai.system_instructions`. Empty string in → single empty text part out\n * (still a valid array, never `undefined`).\n */\nexport function systemInstructionsParts(prompt: string): MessagePart[] {\n return [{ type: \"text\", content: prompt }]\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction coerceRole(raw: unknown): MessageRole {\n if (typeof raw !== \"string\") return \"user\"\n return ALLOWED_ROLES.has(raw as MessageRole) ? (raw as MessageRole) : \"user\"\n}\n\nfunction normalizeBlock(raw: unknown): MessagePart | undefined {\n if (typeof raw === \"string\") return { type: \"text\", content: raw }\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n\n // Already a part.\n if (typeof obj.type === \"string\" && (typeof obj.content === \"string\" || obj.content === undefined)) {\n // If it carries our recognized shape (text / tool_call / tool_call_response /\n // uri), pass through. Otherwise fall through to type-specific normalization.\n if (obj.type === \"text\" && typeof obj.content === \"string\") {\n return { type: \"text\", content: obj.content }\n }\n }\n\n const type = typeof obj.type === \"string\" ? obj.type : \"text\"\n\n if (type === \"text\" && typeof obj.text === \"string\") {\n return { type: \"text\", content: obj.text }\n }\n if (type === \"tool_use\") {\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.input ?? {},\n }\n }\n if (type === \"tool_call\") {\n // Already-normalized tool_call part — pass through.\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.arguments ?? obj.input ?? {},\n }\n }\n if (type === \"tool_result\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.tool_use_id === \"string\" ? obj.tool_use_id : \"\",\n response: obj.content ?? \"\",\n }\n }\n if (type === \"tool_call_response\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n response: obj.response ?? \"\",\n }\n }\n if (type === \"thinking\" && typeof obj.thinking === \"string\") {\n return { type: \"reasoning\", content: obj.thinking }\n }\n if (type === \"reasoning\" && typeof obj.content === \"string\") {\n return { type: \"reasoning\", content: obj.content }\n }\n if (type === \"image\" && obj.source && typeof obj.source === \"object\") {\n const src = obj.source as { media_type?: string; data?: string; url?: string }\n const uri = src.url ?? (src.data ? `data:${src.media_type ?? \"image/unknown\"};base64,${src.data}` : \"\")\n if (uri) return { type: \"uri\", modality: \"image\", uri }\n }\n\n // Unknown block type — stringify so nothing is silently dropped.\n return { type, content: safeJson(raw) }\n}\n\n/**\n * OpenAI assistant messages put tool calls in a separate `tool_calls` array\n * alongside string content. Append them as parts so the trace shows what the\n * model emitted in that turn.\n */\nfunction appendToolCalls(parts: MessagePart[], raw: unknown): void {\n if (!Array.isArray(raw)) return\n for (const tc of raw) {\n if (!tc || typeof tc !== \"object\") continue\n const t = tc as Record<string, unknown>\n const fn = t.function as { name?: string; arguments?: string | Record<string, unknown> } | undefined\n let parsedArgs: unknown = fn?.arguments\n if (typeof parsedArgs === \"string\") {\n try {\n parsedArgs = JSON.parse(parsedArgs)\n } catch {\n // leave as string\n }\n }\n parts.push({\n type: \"tool_call\",\n id: typeof t.id === \"string\" ? t.id : \"\",\n name: fn?.name ?? \"\",\n arguments: parsedArgs ?? {},\n })\n }\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n","import { createHash, randomUUID } from \"node:crypto\"\nimport {\n assistantMessageFromOutput,\n type Message,\n normalizeMessages,\n systemInstructionsParts,\n userMessageFromPrompt,\n} from \"./messages.ts\"\nimport type {\n OpenClawAfterCompactionEvent,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeAgentStartEvent,\n OpenClawBeforeCompactionEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawLlmUsage,\n OpenClawModelCallEndedEvent,\n OpenClawModelCallStartedEvent,\n OpenClawSubagentEndedEvent,\n OpenClawSubagentSpawnedEvent,\n} from \"./types.ts\"\n\n/**\n * Builds the per-trace span tree for an OpenClaw agent run from the granular\n * paired hooks. Replaces the older `turn-builder.ts` model that collapsed the\n * whole attempt into a single `llm_request` span — that shape was wrong on\n * two counts: `llm_input` / `llm_output` fire ONCE per attempt (not per\n * generation), and an attempt is a sequence of generations interleaved with\n * tool executions.\n *\n * Span set this builder produces:\n *\n * agent (root)\n * ├─ compaction (0..1, rare)\n * ├─ model_call (1..N, one per provider API call)\n * ├─ tool_call: ... (interleaved between model_calls; siblings of agent)\n * ├─ subagent (0..N; child agent runs nest INSIDE these via\n * │ └─ agent ... cross-runId trace propagation)\n * └─ model_call (final)\n *\n * Tool spans are siblings of `agent`, not children of `model_call`, because\n * tools run BETWEEN generations — not during them. Nesting under model_call\n * would falsely imply concurrency.\n *\n * `llm_input` / `llm_output` are NOT span boundaries here. They're data-only\n * feeds that enrich the parent `agent` span (full message history, output\n * messages, aggregate token usage).\n */\n\n// ─── Span record shapes ─────────────────────────────────────────────────────\n\nexport interface SpanRecord {\n /** Stable id for the span (16 hex chars). */\n spanId: string\n /** Span tree id (32 hex chars). */\n traceId: string\n /** Empty string for root agent spans, parent's spanId otherwise. */\n parentSpanId: string\n /** OpenClaw event noun (`agent` / `model_call` / `tool_call` / `compaction` / `subagent`). */\n name: string\n startMs: number\n endMs: number | undefined\n /** Free-form attribute bag — flattened to OTLP key/value at emit time. */\n attrs: Record<string, AttrValue>\n /** Status — set at close from the event payload's outcome/error. */\n outcome?: \"ok\" | \"error\"\n errorMessage?: string | undefined\n}\n\nexport type AttrValue = string | number | boolean | unknown[] | Record<string, unknown> | undefined\n\n// One entry per simple `prefix.field` attribute the builder records on a span.\ntype AttrInput = Record<string, AttrValue>\n\n// ─── Per-run state ──────────────────────────────────────────────────────────\n\ninterface RunState {\n /** Trace root span (the `agent`). */\n agent: SpanRecord\n /**\n * Working snapshot of conversation history in the parts-based GenAI shape\n * Latitude's parser expects. Provider-specific shapes from `llm_input` get\n * normalized once on entry; tool_call / tool_call_response parts appended\n * during the run are already in the right shape.\n */\n history: Message[]\n /** Open per-call spans, keyed on the OpenClaw `callId` from `model_call_started`. */\n openModelCalls: Map<string, SpanRecord>\n /** Open tool spans, keyed on `toolCallId`. */\n openToolCalls: Map<string, SpanRecord>\n /** Open compaction span (at most one in flight). */\n openCompaction: SpanRecord | undefined\n /** All closed spans for this run, ready to emit on agent_end. */\n closed: SpanRecord[]\n /** Any subagent spans we OPENED inside this run, keyed by child runId so we\n * can close them when the child's `subagent_ended` arrives. */\n childSubagentSpans: Map<string, SpanRecord>\n}\n\n/**\n * When a parent's `subagent_spawned` fires we register a link from the\n * child's runId → the parent's traceId + the subagent span's id. Then when the\n * child's `before_agent_start` fires, we use those values so the child's\n * entire span subtree lands inside the parent's trace. Outlives the parent's\n * RunState because the child's `agent_end` may arrive after the parent's.\n *\n * `createdAt` is used by the `evictStaleSubagentLinks` sweep to drop entries\n * whose child runs never reached `agent_end` (gateway crash mid-spawn,\n * plugin reload, etc.). Without the sweep the map grows unbounded over the\n * lifetime of a long-running OpenClaw process.\n */\ninterface SubagentLink {\n traceId: string\n /** Span id of the parent's `subagent` span — used as parentSpanId for the child's root. */\n subagentSpanId: string\n createdAt: number\n}\n\nconst SUBAGENT_LINK_TTL_MS = 60 * 60 * 1000 // 1 hour\nconst SUBAGENT_LINK_MAX = 1000\n\nexport interface BuildResult {\n /** Run id this batch belongs to. */\n runId: string\n /** All spans ready to be exported (agent + everything beneath it). */\n spans: SpanRecord[]\n}\n\n// ─── Builder ────────────────────────────────────────────────────────────────\n\nexport class SpanBuilder {\n private readonly runs = new Map<string, RunState>()\n private readonly subagentLinks = new Map<string, SubagentLink>()\n\n inflightCount(): number {\n return this.runs.size\n }\n\n /**\n * Open the root `agent` span. If the runId was previously registered as a\n * subagent's child, propagate the parent's traceId and parent the new span\n * under the parent's `subagent` span — so the entire subagent's work nests\n * inside the parent's trace as one waterfall.\n */\n onBeforeAgentStart(evt: OpenClawBeforeAgentStartEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n if (this.runs.has(runId)) return // defensive — already open\n\n const link = this.subagentLinks.get(runId)\n const traceId = link?.traceId ?? hashHex(runId, 32)\n const parentSpanId = link?.subagentSpanId ?? \"\"\n\n const agent: SpanRecord = {\n spanId: hashHex(`${traceId}:${runId}:agent`, 16),\n traceId,\n parentSpanId,\n name: \"agent\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...flattenCtx(ctx),\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.run.id\": runId,\n // before_agent_start payload — gated content fields go via `gated.*`\n // attribute keys so the OTLP layer can scrub them without a parallel\n // boolean check. Messages get normalized to parts-shape; `prompt` is\n // a plain string (for the user-prompt convenience attribute).\n \"before_agent_start.prompt:gated\": evt.prompt,\n \"before_agent_start.messages:gated\": evt.messages ? normalizeMessages(evt.messages) : undefined,\n },\n }\n\n this.runs.set(runId, {\n agent,\n history: [],\n openModelCalls: new Map(),\n openToolCalls: new Map(),\n openCompaction: undefined,\n closed: [],\n childSubagentSpans: new Map(),\n })\n }\n\n /**\n * Enrich the open `agent` span with content + identity from the LLM input.\n * Also seeds the rolling history snapshot used by per-call `model_call`\n * input attributes.\n *\n * Provider-specific message shapes get normalized into the parts-based\n * GenAI format here — that's the contract Latitude's downstream parser\n * expects on `gen_ai.input.messages` and `gen_ai.system_instructions`.\n */\n onLlmInput(evt: OpenClawLlmInputEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(ctx.runId ?? evt.runId)\n if (!run) return\n\n const normalizedHistory = normalizeMessages(evt.historyMessages)\n const inputMessages: Message[] = [...normalizedHistory]\n if (evt.prompt) inputMessages.push(userMessageFromPrompt(evt.prompt))\n\n Object.assign(run.agent.attrs, {\n \"gen_ai.system_instructions:gated\": evt.systemPrompt ? systemInstructionsParts(evt.systemPrompt) : undefined,\n \"user_prompt:gated\": evt.prompt,\n \"gen_ai.input.messages:gated\": inputMessages,\n \"openclaw.images.count\": evt.imagesCount,\n \"gen_ai.request.model\": evt.model,\n \"gen_ai.system\": evt.provider,\n \"openclaw.provider\": evt.provider,\n })\n // Seed the rolling history with the normalized form. The per-call\n // model_call_started will copy the snapshot at the time it fires;\n // subsequent before_tool_call / after_tool_call events append to the\n // same array (already in parts shape) so the next model_call captures\n // the post-tool state.\n run.history = inputMessages\n }\n\n /**\n * Enrich the agent span with attempt-aggregate output + token usage.\n * (Per-call usage isn't surfaced by OpenClaw today — see PR #2986.)\n */\n onLlmOutput(evt: OpenClawLlmOutputEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(ctx.runId ?? evt.runId)\n if (!run) return\n const assistantMessage = assistantMessageFromOutput(evt.assistantTexts, evt.lastAssistant)\n Object.assign(run.agent.attrs, {\n \"gen_ai.output.messages:gated\": [assistantMessage],\n \"openclaw.resolved.ref\": evt.resolvedRef,\n \"openclaw.harness.id\": evt.harnessId,\n \"gen_ai.response.model\": evt.model,\n ...usageAttrs(evt.usage),\n })\n }\n\n onModelCallStarted(evt: OpenClawModelCallStartedEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(evt.runId)\n if (!run) return\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:model_call:${evt.callId}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: \"model_call\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.run.id\": evt.runId,\n \"openclaw.call.id\": evt.callId,\n \"gen_ai.system\": evt.provider,\n \"openclaw.provider\": evt.provider,\n \"gen_ai.request.model\": evt.model,\n \"openclaw.api\": evt.api,\n \"openclaw.transport\": evt.transport,\n // Snapshot the rolling history at the moment this generation starts.\n // The model saw exactly this state. Gated.\n \"gen_ai.input.messages:gated\": [...run.history],\n },\n }\n run.openModelCalls.set(evt.callId, span)\n }\n\n onModelCallEnded(evt: OpenClawModelCallEndedEvent, _ctx: OpenClawAgentContext): void {\n const run = this.runs.get(evt.runId)\n if (!run) return\n const span = run.openModelCalls.get(evt.callId)\n if (!span) return\n span.endMs = Date.now()\n span.outcome = evt.outcome === \"completed\" ? \"ok\" : \"error\"\n span.errorMessage = evt.errorCategory\n Object.assign(span.attrs, {\n \"openclaw.duration_ms\": evt.durationMs,\n \"openclaw.outcome\": evt.outcome,\n \"openclaw.error.category\": evt.errorCategory,\n \"openclaw.failure.kind\": evt.failureKind,\n \"openclaw.request.payload_bytes\": evt.requestPayloadBytes,\n \"openclaw.response.stream_bytes\": evt.responseStreamBytes,\n \"openclaw.ttfb_ms\": evt.timeToFirstByteMs,\n \"openclaw.upstream.request_id_hash\": evt.upstreamRequestIdHash,\n })\n run.openModelCalls.delete(evt.callId)\n run.closed.push(span)\n }\n\n /**\n * Open a `tool_call` span as a sibling of the agent span. Also append a\n * synthetic assistant `tool_call` part to the rolling history so the NEXT\n * model_call's input snapshot reflects what the model emitted.\n *\n * IMPORTANT: this runs as a `runModifyingHook` in OpenClaw — returning\n * anything other than `undefined`/falsy from this handler blocks the tool.\n * The plugin-side handler enforces a void return; this method's signature\n * already returns `void`.\n */\n onBeforeToolCall(evt: OpenClawBeforeToolCallEvent, ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n\n const toolCallId = evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:tool_call:${toolCallId}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: `tool_call:${evt.toolName}`,\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.run.id\": evt.runId,\n \"gen_ai.tool.name\": evt.toolName,\n \"gen_ai.tool.call.id\": toolCallId,\n \"gen_ai.tool.call.arguments:gated\": evt.params,\n },\n }\n run.openToolCalls.set(toolCallId, span)\n\n // Append an assistant tool_call part to the rolling history so the next\n // model_call captures it.\n run.history.push({\n role: \"assistant\",\n parts: [{ type: \"tool_call\", id: toolCallId, name: evt.toolName, arguments: evt.params }],\n })\n }\n\n onAfterToolCall(evt: OpenClawAfterToolCallEvent, _ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n\n // Match priority:\n // 1. Direct id lookup — the happy path.\n // 2. Name-match fallback — if `evt.toolCallId` is missing OR refers to\n // an id we didn't open (e.g. before_tool_call elided it and we\n // synthesised one, then after_tool_call provided the real one).\n //\n // Without the id-mismatch fallback, the open span would never close\n // and would get force-closed as `abandoned` at agent_end.\n let resolvedId: string | undefined =\n evt.toolCallId && run.openToolCalls.has(evt.toolCallId) ? evt.toolCallId : undefined\n if (!resolvedId) resolvedId = this.findOpenToolCallByName(run, evt.toolName)\n if (!resolvedId) return\n const span = run.openToolCalls.get(resolvedId)\n if (!span) return\n const toolCallId: string = resolvedId\n\n span.endMs = Date.now()\n const isError = Boolean(evt.error)\n span.outcome = isError ? \"error\" : \"ok\"\n span.errorMessage = evt.error\n Object.assign(span.attrs, {\n \"gen_ai.tool.call.result:gated\": evt.result,\n \"openclaw.error.message:gated\": evt.error,\n \"openclaw.duration_ms\": evt.durationMs,\n })\n run.openToolCalls.delete(toolCallId)\n run.closed.push(span)\n\n // Append the tool response to the rolling history so the next\n // model_call's input snapshot includes it.\n run.history.push({\n role: \"tool\",\n parts: [{ type: \"tool_call_response\", id: toolCallId, response: evt.result ?? evt.error ?? \"\" }],\n })\n }\n\n onBeforeCompaction(evt: OpenClawBeforeCompactionEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n const run = this.runs.get(runId)\n if (!run) return\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:compaction:${run.closed.length}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: \"compaction\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.run.id\": runId,\n \"openclaw.compaction.message_count.before\": evt.messageCount,\n \"openclaw.compaction.session_file\": evt.sessionFile,\n \"before_compaction.messages:gated\": evt.messages ? normalizeMessages(evt.messages) : undefined,\n },\n }\n run.openCompaction = span\n }\n\n onAfterCompaction(evt: OpenClawAfterCompactionEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n const run = this.runs.get(runId)\n if (!run?.openCompaction) return\n const span = run.openCompaction\n span.endMs = Date.now()\n span.outcome = \"ok\"\n Object.assign(span.attrs, {\n \"openclaw.compaction.message_count.after\": evt.messageCount,\n \"openclaw.compaction.compacted_count\": evt.compactedCount,\n \"openclaw.compaction.token_count\": evt.tokenCount,\n })\n run.openCompaction = undefined\n run.closed.push(span)\n }\n\n /**\n * Open a `subagent` span on the parent run AND register the cross-run link\n * so the child's `before_agent_start` can find us.\n */\n onSubagentSpawned(evt: OpenClawSubagentSpawnedEvent, ctx: OpenClawAgentContext): void {\n const parentRunId = ctx.runId\n if (!parentRunId) return\n const parent = this.runs.get(parentRunId)\n if (!parent) return\n\n const span: SpanRecord = {\n spanId: hashHex(`${parent.agent.traceId}:subagent:${evt.runId}`, 16),\n traceId: parent.agent.traceId,\n parentSpanId: parent.agent.spanId,\n name: \"subagent\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n // The subagent span lives inside the parent's trace — tags +\n // metadata reflect the parent's ctx (which agent/channel/trigger\n // is doing the spawning), not the child's. The child's `agent` span\n // (opened later by the child run's before_agent_start) carries\n // its OWN ctx.\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.parent.run.id\": parentRunId,\n \"openclaw.run.id\": evt.runId,\n \"openclaw.subagent.child_session_key\": evt.childSessionKey,\n \"openclaw.subagent.agent_id\": evt.agentId,\n \"openclaw.subagent.label\": evt.label,\n \"openclaw.subagent.mode\": evt.mode,\n \"openclaw.subagent.thread_requested\": evt.threadRequested,\n \"openclaw.subagent.requester.channel\": evt.requester?.channel,\n \"openclaw.subagent.requester.account_id\": evt.requester?.accountId,\n \"openclaw.subagent.requester.to\": evt.requester?.to,\n \"openclaw.subagent.requester.thread_id\":\n typeof evt.requester?.threadId === \"string\" || typeof evt.requester?.threadId === \"number\"\n ? String(evt.requester.threadId)\n : undefined,\n },\n }\n parent.childSubagentSpans.set(evt.runId, span)\n this.evictStaleSubagentLinks()\n this.subagentLinks.set(evt.runId, {\n traceId: parent.agent.traceId,\n subagentSpanId: span.spanId,\n createdAt: Date.now(),\n })\n }\n\n onSubagentEnded(evt: OpenClawSubagentEndedEvent, ctx: OpenClawAgentContext): void {\n const parentRunId = ctx.runId\n if (!parentRunId) return\n const parent = this.runs.get(parentRunId)\n if (!parent) return\n const childRunId = evt.runId\n if (!childRunId) return\n const span = parent.childSubagentSpans.get(childRunId)\n if (!span) return\n\n span.endMs = Date.now()\n const isError = evt.outcome === \"error\" || Boolean(evt.error)\n span.outcome = isError ? \"error\" : \"ok\"\n span.errorMessage = evt.error\n Object.assign(span.attrs, {\n \"openclaw.subagent.target_session_key\": evt.targetSessionKey,\n \"openclaw.subagent.target_kind\": evt.targetKind,\n \"openclaw.subagent.reason\": evt.reason,\n \"openclaw.subagent.outcome\": evt.outcome,\n \"openclaw.subagent.send_farewell\": evt.sendFarewell,\n \"openclaw.subagent.account_id\": evt.accountId,\n })\n parent.childSubagentSpans.delete(childRunId)\n parent.closed.push(span)\n // Don't delete the subagent link yet — the child's `agent_end` may still\n // be in flight. We clean it up when the child's agent_end fires.\n }\n\n /**\n * Close out the run: finish the agent span, abandon any still-open\n * model_calls / tool_calls / compactions, and return everything ready to\n * emit. Removes the subagent link if this was a child run.\n */\n onAgentEnd(evt: OpenClawAgentEndEvent, ctx: OpenClawAgentContext): BuildResult | undefined {\n const runId = ctx.runId\n if (!runId) return undefined\n const run = this.runs.get(runId)\n if (!run) {\n // Child agent run that closed without ever opening — drop the link\n // entry so the map doesn't grow unbounded.\n this.subagentLinks.delete(runId)\n return undefined\n }\n\n const now = Date.now()\n run.agent.endMs = now\n run.agent.outcome = evt.success ? \"ok\" : \"error\"\n run.agent.errorMessage = evt.error\n Object.assign(run.agent.attrs, {\n \"openclaw.duration_ms\": evt.durationMs,\n \"openclaw.run.success\": evt.success,\n \"openclaw.error.message:gated\": evt.error,\n \"agent_end.messages:gated\": normalizeMessages(evt.messages),\n })\n\n // Anything still open at agent_end didn't get a proper close event.\n // Mark them abandoned and force-close so they show up in the trace\n // rather than vanish silently.\n for (const span of run.openModelCalls.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n for (const span of run.openToolCalls.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n if (run.openCompaction) {\n run.openCompaction.endMs = now\n run.openCompaction.outcome = \"error\"\n run.openCompaction.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(run.openCompaction)\n }\n for (const span of run.childSubagentSpans.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.subagent.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n\n const spans = [run.agent, ...run.closed]\n this.runs.delete(runId)\n this.subagentLinks.delete(runId)\n return { runId, spans }\n }\n\n /** Drop a run without emitting — used on errors from the emit path. */\n abandon(runId: string): void {\n this.runs.delete(runId)\n this.subagentLinks.delete(runId)\n }\n\n /** Test-only: how many cross-run subagent links we're holding. */\n subagentLinkCount(): number {\n return this.subagentLinks.size\n }\n\n /**\n * Drop any subagent links whose child run never reached `agent_end`. Called\n * before every `subagent_spawned` insert so the map stays bounded even when\n * children crash mid-spawn or the plugin reloads.\n *\n * Two passes: TTL eviction (anything older than `SUBAGENT_LINK_TTL_MS`),\n * then a hard size cap (when we're past `SUBAGENT_LINK_MAX`, drop the\n * oldest until we're under).\n */\n private evictStaleSubagentLinks(): void {\n const now = Date.now()\n for (const [runId, link] of this.subagentLinks) {\n if (now - link.createdAt > SUBAGENT_LINK_TTL_MS) {\n this.subagentLinks.delete(runId)\n }\n }\n if (this.subagentLinks.size <= SUBAGENT_LINK_MAX) return\n const sorted = Array.from(this.subagentLinks.entries()).sort((a, b) => a[1].createdAt - b[1].createdAt)\n const toRemove = this.subagentLinks.size - SUBAGENT_LINK_MAX\n for (let i = 0; i < toRemove; i++) {\n const entry = sorted[i]\n if (entry) this.subagentLinks.delete(entry[0])\n }\n }\n\n private findOpenToolCallByName(run: RunState, toolName: string): string | undefined {\n // Defensive: OpenClaw versions that elide toolCallId on after_tool_call\n // can be matched by name + still-open status. When multiple in-flight\n // tool calls share a name, prefer the MOST RECENTLY opened — Maps\n // preserve insertion order, so iterating in reverse picks the latest.\n // (LIFO matches typical agent runtimes that issue tools sequentially.)\n const target = `tool_call:${toolName}`\n const entries = Array.from(run.openToolCalls.entries())\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i]\n if (!entry) continue\n const [id, span] = entry\n if (span.name === target) return id\n }\n return undefined\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction flattenCtx(ctx: OpenClawAgentContext): AttrInput {\n return {\n \"openclaw.run.id\": ctx.runId,\n \"openclaw.session.id\": ctx.sessionId,\n \"openclaw.session.key\": ctx.sessionKey,\n \"openclaw.agent.id\": ctx.agentId,\n \"openclaw.agent.name\": ctx.agentId,\n \"openclaw.workspace.dir\": ctx.workspaceDir,\n \"openclaw.message.provider\": ctx.messageProvider,\n \"openclaw.trigger\": ctx.trigger,\n \"openclaw.channel.id\": ctx.channelId,\n \"openclaw.cron.job.id\": ctx.jobId,\n \"openclaw.model.provider.id\": ctx.modelProviderId,\n \"openclaw.model.id\": ctx.modelId,\n }\n}\n\n/**\n * Mirror OpenClaw's session id onto the OTEL-standard keys Latitude's\n * resolver looks for. `gen_ai.session.id` and `session.id` are both in\n * `sessionIdCandidates` (domain/spans/src/otlp/resolvers/identity.ts), so\n * traces can be grouped by session in the Latitude UI without an\n * openclaw-specific code path. Emitted on every span, not just `agent`,\n * so child spans (model_call / tool_call / etc.) inherit the same grouping.\n */\nfunction sessionAttrs(ctx: OpenClawAgentContext): AttrInput {\n if (!ctx.sessionId) return {}\n return {\n \"session.id\": ctx.sessionId,\n \"gen_ai.session.id\": ctx.sessionId,\n }\n}\n\n/**\n * Build `latitude.tags` and `latitude.metadata` attrs from the hook context.\n * The OTLP encoder JSON-stringifies arrays/objects, which is the encoding\n * Latitude's resolver expects:\n *\n * - `latitude.tags` is a JSON-encoded string array (`fromJsonStringArray`\n * in domain/spans/src/otlp/resolvers/enrichment.ts).\n * - `latitude.metadata` is a JSON-encoded string object (`fromJsonString`).\n *\n * Tags = the agent id, the channel id, and the trigger. When trigger is\n * `cron`, the tag becomes `cron:<jobId>` so dashboards can pivot on the\n * specific cron job. Each tag is conditionally included so absent ctx\n * fields don't produce empty entries.\n *\n * Metadata = every ctx field that's set, namespaced under `openclaw.*` so\n * it can't collide with metadata keys other plugins might emit.\n */\nfunction latitudeAttrs(ctx: OpenClawAgentContext): AttrInput {\n const tags: string[] = []\n if (ctx.agentId) tags.push(ctx.agentId)\n if (ctx.channelId) tags.push(ctx.channelId)\n if (ctx.trigger) {\n tags.push(ctx.trigger === \"cron\" && ctx.jobId ? `cron:${ctx.jobId}` : ctx.trigger)\n }\n\n const metadata: Record<string, string> = {}\n if (ctx.runId) metadata[\"openclaw.run.id\"] = ctx.runId\n if (ctx.sessionId) metadata[\"openclaw.session.id\"] = ctx.sessionId\n if (ctx.sessionKey) metadata[\"openclaw.session.key\"] = ctx.sessionKey\n if (ctx.agentId) metadata[\"openclaw.agent.id\"] = ctx.agentId\n if (ctx.workspaceDir) metadata[\"openclaw.workspace.dir\"] = ctx.workspaceDir\n if (ctx.channelId) metadata[\"openclaw.channel.id\"] = ctx.channelId\n if (ctx.messageProvider) metadata[\"openclaw.message.provider\"] = ctx.messageProvider\n if (ctx.trigger) metadata[\"openclaw.trigger\"] = ctx.trigger\n if (ctx.jobId) metadata[\"openclaw.cron.job.id\"] = ctx.jobId\n if (ctx.modelProviderId) metadata[\"openclaw.model.provider.id\"] = ctx.modelProviderId\n if (ctx.modelId) metadata[\"openclaw.model.id\"] = ctx.modelId\n\n return {\n \"latitude.tags\": tags.length > 0 ? tags : undefined,\n \"latitude.metadata\": Object.keys(metadata).length > 0 ? metadata : undefined,\n }\n}\n\nfunction usageAttrs(usage: OpenClawLlmUsage | undefined): AttrInput {\n if (!usage) return {}\n return {\n \"gen_ai.usage.input_tokens\": usage.input,\n \"gen_ai.usage.output_tokens\": usage.output,\n \"gen_ai.usage.cache_read_input_tokens\": usage.cacheRead,\n \"gen_ai.usage.cache_creation_input_tokens\": usage.cacheWrite,\n \"gen_ai.usage.total_tokens\": usage.total,\n }\n}\n\nfunction hashHex(input: string, length: number): string {\n return createHash(\"sha256\").update(input).digest(\"hex\").slice(0, length)\n}\n","import { postTraces } from \"./client.ts\"\nimport { type Config, loadConfig } from \"./config.ts\"\nimport { createLogger, type Logger } from \"./logger.ts\"\nimport { buildOtlpRequest } from \"./otlp.ts\"\nimport { type BuildResult, SpanBuilder } from \"./span-builder.ts\"\nimport type {\n OpenClawAfterCompactionEvent,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeAgentStartEvent,\n OpenClawBeforeCompactionEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawModelCallEndedEvent,\n OpenClawModelCallStartedEvent,\n OpenClawSubagentEndedEvent,\n OpenClawSubagentSpawnedEvent,\n} from \"./types.ts\"\n\n/**\n * Minimal structural type for OpenClaw's plugin API — only the fields we\n * touch. We avoid importing from `openclaw/plugin-sdk` so the package stays\n * usable when OpenClaw isn't installed (the CLI and tests don't need it),\n * and so we're robust to small signature changes across OpenClaw versions.\n *\n * `pluginConfig` is the user's `plugins.entries[id].config` block — that's\n * the canonical place to read credentials and feature flags. The OpenClaw\n * plugin SDK also exposes the same value as `api.pluginConfig` on the\n * builder API; keep both names in sync if the upstream contract evolves.\n */\nexport interface OpenClawPluginApiLike {\n logger?: Logger\n pluginConfig?: Record<string, unknown>\n on: <K extends string>(\n hookName: K,\n handler: (event: unknown, ctx: unknown) => unknown,\n opts?: { priority?: number },\n ) => void\n}\n\nexport interface RegisterOptions {\n /** Override the config, mostly for tests. */\n config?: Config\n /** Override the logger. */\n logger?: Logger\n /**\n * Hook to observe the emitted run right before it's posted. Used by tests;\n * not a stable public API.\n */\n onEmit?: (result: BuildResult) => void\n}\n\n/**\n * Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls\n * this once at plugin activation; we wire up the granular paired hooks\n * (model_call_started/_ended, before_/after_tool_call, before_/after_compaction,\n * subagent_spawned/_ended, before_agent_start/agent_end) plus the\n * data-only feeds (llm_input/llm_output) that enrich the agent span.\n *\n * Every typed hook on OpenClaw's side fires fire-and-forget for non-modifying\n * hooks; before_tool_call is a `runModifyingHook` where returning anything\n * other than undefined blocks the tool call. Our handler returns nothing —\n * keep it that way.\n */\nexport default function registerLatitudePlugin(api: OpenClawPluginApiLike, opts: RegisterOptions = {}): void {\n // Source of truth: OpenClaw passes the user's `plugins.entries[id].config`\n // as `api.pluginConfig`. Env vars are a fallback so existing deploys with\n // LATITUDE_* exported in the gateway environment keep working.\n const config = opts.config ?? loadConfig(api.pluginConfig)\n const logger = opts.logger ?? createLogger(config.debug)\n\n if (!config.enabled) {\n if (config.apiKey === \"\") logger.debug(\"disabled: apiKey is empty (set plugins.entries[id].config.apiKey)\")\n if (config.project === \"\") logger.debug(\"disabled: project is empty (set plugins.entries[id].config.project)\")\n return\n }\n logger.debug(\n `enabled: project=${config.project} base=${config.baseUrl} allowConversationAccess=${config.allowConversationAccess}`,\n )\n\n const builder = new SpanBuilder()\n\n // Helper: wrap a void-returning hook handler with try/catch + cast.\n const wrap = <E>(\n name: string,\n fn: (evt: E, ctx: OpenClawAgentContext) => void,\n ): ((evt: unknown, ctx: unknown) => void) => {\n return (evt, ctx) => {\n try {\n fn(evt as E, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`${name} handler failed: ${String(err)}`)\n }\n }\n }\n\n // ─── Span boundaries ────────────────────────────────────────────────────\n\n api.on(\n \"before_agent_start\",\n wrap<OpenClawBeforeAgentStartEvent>(\"before_agent_start\", (evt, ctx) => {\n builder.onBeforeAgentStart(evt, ctx)\n }),\n )\n\n api.on(\n \"model_call_started\",\n wrap<OpenClawModelCallStartedEvent>(\"model_call_started\", (evt, ctx) => {\n builder.onModelCallStarted(evt, ctx)\n }),\n )\n api.on(\n \"model_call_ended\",\n wrap<OpenClawModelCallEndedEvent>(\"model_call_ended\", (evt, ctx) => {\n builder.onModelCallEnded(evt, ctx)\n }),\n )\n\n // before_tool_call is a `runModifyingHook` — returning {block: true} from\n // any plugin handler blocks the tool. We return nothing (void) so OpenClaw\n // dispatches normally. The `wrap` helper preserves that void return.\n api.on(\n \"before_tool_call\",\n wrap<OpenClawBeforeToolCallEvent>(\"before_tool_call\", (evt, ctx) => {\n builder.onBeforeToolCall(evt, ctx)\n }),\n )\n api.on(\n \"after_tool_call\",\n wrap<OpenClawAfterToolCallEvent>(\"after_tool_call\", (evt, ctx) => {\n builder.onAfterToolCall(evt, ctx)\n }),\n )\n\n api.on(\n \"before_compaction\",\n wrap<OpenClawBeforeCompactionEvent>(\"before_compaction\", (evt, ctx) => {\n builder.onBeforeCompaction(evt, ctx)\n }),\n )\n api.on(\n \"after_compaction\",\n wrap<OpenClawAfterCompactionEvent>(\"after_compaction\", (evt, ctx) => {\n builder.onAfterCompaction(evt, ctx)\n }),\n )\n\n api.on(\n \"subagent_spawned\",\n wrap<OpenClawSubagentSpawnedEvent>(\"subagent_spawned\", (evt, ctx) => {\n builder.onSubagentSpawned(evt, ctx)\n }),\n )\n api.on(\n \"subagent_ended\",\n wrap<OpenClawSubagentEndedEvent>(\"subagent_ended\", (evt, ctx) => {\n builder.onSubagentEnded(evt, ctx)\n }),\n )\n\n // ─── Data-only feeds ────────────────────────────────────────────────────\n // These DON'T open or close spans. They enrich the open `agent` span with\n // attempt-aggregate content + token usage, and seed the rolling history\n // snapshot used by per-call `model_call.input.messages`.\n\n api.on(\n \"llm_input\",\n wrap<OpenClawLlmInputEvent>(\"llm_input\", (evt, ctx) => {\n builder.onLlmInput(evt, ctx)\n }),\n )\n api.on(\n \"llm_output\",\n wrap<OpenClawLlmOutputEvent>(\"llm_output\", (evt, ctx) => {\n builder.onLlmOutput(evt, ctx)\n }),\n )\n\n // ─── Trace flush ────────────────────────────────────────────────────────\n //\n // Why we defer the finalize by one microtask tick instead of finalizing\n // synchronously inside the agent_end handler: OpenClaw 2026.4.26+ has TWO\n // hook fire-orders depending on which runtime the agent uses, and the\n // selection.runtime path (used by the codex / embedded ACPX agents) fires\n // events in this order:\n //\n // llm_input → ...model_calls / tool_calls... → agent_end → llm_output\n //\n // The cli-runner.runtime path (used by the claude-code agent) fires the\n // reverse — `llm_output` BEFORE `agent_end` — and only when the assistant\n // emitted a non-empty text part.\n //\n // If we finalize on agent_end synchronously, the run is deleted and the OTLP\n // batch is shipped before `llm_output` (under selection.runtime) gets a\n // chance to enrich the agent span with `gen_ai.output.messages`,\n // `gen_ai.response.model`, `openclaw.resolved.ref`, `openclaw.harness.id`,\n // and the entire `gen_ai.usage.*` block. The `onLlmOutput` handler then\n // bails on `if (!run) return` cleanly (no error, no warning), and every\n // attribute that lives on the `llm_output` event is silently dropped.\n //\n // Deferring with `queueMicrotask` is order-agnostic: in either path, both\n // hook handlers run synchronously in the current microtask round and write\n // to the still-alive run; the queued finalize fires after both have\n // completed and serializes a fully-enriched batch. Subagents go through\n // exactly the same `onAgentEnd` path so they benefit automatically.\n //\n // We don't use `setTimeout(0)` because the +1 macrotask of latency isn't\n // meaningful here, and `queueMicrotask` is more reliable on process exit\n // (microtasks drain before exit; macrotasks may not). If a future OpenClaw\n // ever introduces an `await` between `agent_end` dispatch and `llm_output`\n // dispatch, we'll need to switch to `setTimeout(0)`.\n api.on(\n \"agent_end\",\n wrap<OpenClawAgentEndEvent>(\"agent_end\", (evt, ctx) => {\n queueMicrotask(() => {\n try {\n const result = builder.onAgentEnd(evt, ctx)\n if (!result) {\n logger.debug(\"agent_end fired without a matching run in flight\")\n return\n }\n opts.onEmit?.(result)\n const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess })\n void postTraces({\n baseUrl: config.baseUrl,\n apiKey: config.apiKey,\n project: config.project,\n payload,\n logger,\n })\n } catch (err) {\n logger.warn(`agent_end finalize failed: ${String(err)}`)\n }\n })\n }),\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":["safeJson"],"sources":["../src/client.ts","../src/redaction.ts","../src/config.ts","../src/logger.ts","../src/otlp.ts","../src/messages.ts","../src/span-builder.ts","../src/plugin.ts"],"sourcesContent":["import type { Logger } from \"./logger.ts\"\nimport type { OtlpExportRequest } from \"./types.ts\"\n\nexport async function postTraces({\n baseUrl,\n apiKey,\n project,\n payload,\n logger,\n timeoutMs = 10_000,\n}: {\n baseUrl: string\n apiKey: string\n project: string\n payload: OtlpExportRequest\n logger: Logger\n timeoutMs?: number\n}): Promise<void> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/v1/traces`\n const bodyText = JSON.stringify(payload)\n logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`)\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"X-Latitude-Project\": project,\n },\n body: bodyText,\n signal: controller.signal,\n })\n if (!res.ok) {\n const text = await res.text().catch(() => \"\")\n logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`)\n } else {\n logger.debug(`ingest HTTP ${res.status}`)\n }\n } catch (err) {\n logger.warn(`ingest failed: ${String(err)}`)\n } finally {\n clearTimeout(timer)\n }\n}\n","import type { OtlpKeyValue } from \"./types.ts\"\n\nexport interface RedactConfig {\n attributes: string[]\n mask: string\n}\n\nexport function parseRedactConfig(value: unknown): RedactConfig | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined\n const obj = value as Record<string, unknown>\n const attributes = parseAttributes(obj.attributes)\n if (attributes.length === 0) return undefined\n return { attributes, mask: typeof obj.mask === \"string\" ? obj.mask : \"******\" }\n}\n\nexport function redactAttributes(attributes: OtlpKeyValue[], config: RedactConfig | undefined): OtlpKeyValue[] {\n if (!config) return attributes\n const matchers = config.attributes.map(toMatcher).filter((matcher): matcher is (key: string) => boolean => !!matcher)\n if (matchers.length === 0) return attributes\n return attributes.map((attr) =>\n matchers.some((matches) => matches(attr.key)) ? redactedAttr(attr.key, config.mask) : attr,\n )\n}\n\nfunction parseAttributes(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return value.filter((item): item is string => typeof item === \"string\" && item.trim() !== \"\")\n}\n\nfunction toMatcher(pattern: string): ((key: string) => boolean) | undefined {\n if (pattern.startsWith(\"/\") && pattern.lastIndexOf(\"/\") > 0) {\n const end = pattern.lastIndexOf(\"/\")\n try {\n const regex = new RegExp(pattern.slice(1, end), pattern.slice(end + 1))\n return (key) => {\n regex.lastIndex = 0\n return regex.test(key)\n }\n } catch {\n return undefined\n }\n }\n try {\n const regex = new RegExp(pattern)\n return (key) => {\n regex.lastIndex = 0\n return key === pattern || regex.test(key)\n }\n } catch {\n return (key) => key === pattern\n }\n}\n\nfunction redactedAttr(key: string, mask: string): OtlpKeyValue {\n return { key, value: { stringValue: mask } }\n}\n","import { parseRedactConfig, type RedactConfig } from \"./redaction.ts\"\n\nexport interface Config {\n apiKey: string\n baseUrl: string\n project: string\n enabled: boolean\n debug: boolean\n /**\n * When false, the plugin still emits one span per LLM call / tool / run, but\n * scrubs raw conversation content (input/output messages, system prompt,\n * tool args, tool results, the surfaced first-prompt). Token counts, model\n * names, agent ids, and timings are unaffected.\n */\n allowConversationAccess: boolean\n redact?: RedactConfig | undefined\n}\n\nconst DEFAULT_BASE_URL = \"https://ingest.latitude.so\"\n\n/**\n * Build a `Config` from OpenClaw's per-plugin config bucket. The plugin SDK\n * passes `api.pluginConfig` (the user's `plugins.entries[id].config` block)\n * to the registration function — that's the only source.\n *\n * Earlier 0.0.x versions also fell back to environment variables when keys\n * were missing from pluginConfig. That fallback is gone deliberately:\n * OpenClaw 2026.4.25's `openclaw plugins install` runs a static-analysis\n * security scan that flags any runtime source combining environment-variable\n * access with a network-send call (we have `fetch(` in postTraces). With\n * the fallback our bundled runtime tripped the scanner. The installer\n * writes credentials to `plugins.entries[id].config` anyway, so the\n * fallback was polish-not-feature — its removal also gives a cleaner\n * privacy story (the runtime can't pick up credentials the operator\n * didn't put in openclaw.json).\n *\n * For dev-time testing with debug logs, set `config.debug = true` in\n * openclaw.json directly.\n */\nexport function loadConfig(pluginConfig: Record<string, unknown> | undefined = undefined): Config {\n const fromOpts = pluginConfig ?? {}\n\n const apiKey = pickString(fromOpts.apiKey) ?? \"\"\n const project = pickString(fromOpts.project) ?? \"\"\n const baseUrl = pickString(fromOpts.baseUrl) ?? DEFAULT_BASE_URL\n\n const debug = pickBool(fromOpts.debug) ?? false\n const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false\n\n const explicitlyDisabled = pickBool(fromOpts.enabled) === false\n const hasCreds = apiKey !== \"\" && project !== \"\"\n\n return {\n apiKey,\n baseUrl,\n project,\n debug,\n allowConversationAccess,\n redact: parseRedactConfig(fromOpts.redact),\n enabled: hasCreds && !explicitlyDisabled,\n }\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined\n}\n\nfunction pickBool(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined\n}\n","const PREFIX = \"[latitude-openclaw]\"\n\nexport interface Logger {\n debug: (msg: string) => void\n warn: (msg: string) => void\n}\n\nexport function createLogger(debugEnabled: boolean): Logger {\n return {\n debug: debugEnabled ? (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`) : () => {},\n warn: (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`),\n }\n}\n","import { arch, hostname, platform, release } from \"node:os\"\nimport { type RedactConfig, redactAttributes } from \"./redaction.ts\"\nimport type { AttrValue, BuildResult, SpanRecord } from \"./span-builder.ts\"\nimport type { OtlpExportRequest, OtlpKeyValue, OtlpResourceSpans, OtlpSpan } from \"./types.ts\"\n\nconst SCOPE_NAME = \"@latitude-data/openclaw-telemetry\"\n\n/**\n * Build-time-baked package version.\n *\n * `__SCOPE_VERSION__` is replaced at bundle time by tsdown's `define` (see\n * `tsdown.config.ts`) with a string literal of `package.json`'s `version`,\n * so the released bundle ships a constant — no runtime file read.\n *\n * Earlier versions read `package.json` at runtime via `readFileSync` to keep\n * one source of truth for the version. That tripped OpenClaw 2026.4.26's\n * `plugins.code_safety` scanner with a \"potential-exfiltration: File read\n * combined with network send\" warning (we have `fetch(` in `client.ts`).\n * Build-time bake preserves the single source of truth (the build reads\n * `package.json` and inlines the value) while keeping the runtime free of\n * `node:fs`.\n *\n * The `typeof` check is a runtime fallback for environments where the\n * `define` substitution didn't run — chiefly vitest, which executes the\n * source files directly without going through the build. `typeof` of an\n * undeclared identifier returns `\"undefined\"` rather than throwing, which\n * keeps tests working.\n */\ndeclare const __SCOPE_VERSION__: string\nconst SCOPE_VERSION = typeof __SCOPE_VERSION__ === \"string\" ? __SCOPE_VERSION__ : \"0.0.0-dev\"\n\ninterface BuildOptions {\n /**\n * When false, attributes whose key ends in `:gated` are scrubbed from\n * spans before export — that's `gen_ai.input.messages`,\n * `gen_ai.output.messages`, `gen_ai.system_instructions`, `user_prompt`,\n * `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result`,\n * `before_compaction.messages`, `before_agent_start.{prompt,messages}`,\n * `agent_end.messages`, and `openclaw.error.message` (the last because\n * error strings can leak prompt/response content). Timing, token usage,\n * model name, ids, agent name, durations, byte counts, and the\n * `latitude.captured.content` boolean are always emitted.\n */\n allowConversationAccess: boolean\n redact?: RedactConfig | undefined\n}\n\n/** Build an OTLP export request for a single completed agent run. */\nexport function buildOtlpRequest(result: BuildResult, options: BuildOptions): OtlpExportRequest {\n const spans = result.spans.map((span) => toOtlpSpan(span, options))\n const rs: OtlpResourceSpans = {\n resource: { attributes: resourceAttrs() },\n scopeSpans: [{ scope: { name: SCOPE_NAME, version: SCOPE_VERSION }, spans }],\n }\n return { resourceSpans: [rs] }\n}\n\n// ─── SpanRecord → OtlpSpan ─────────────────────────────────────────────────\n\nfunction toOtlpSpan(span: SpanRecord, options: BuildOptions): OtlpSpan {\n const startNs = msToNs(span.startMs)\n const endNs = msToNs(span.endMs ?? span.startMs)\n\n const attrs: OtlpKeyValue[] = []\n for (const [rawKey, value] of Object.entries(span.attrs)) {\n if (value === undefined || value === null) continue\n const isGated = rawKey.endsWith(\":gated\")\n if (isGated && !options.allowConversationAccess) continue\n const key = isGated ? rawKey.slice(0, -\":gated\".length) : rawKey\n const kv = encodeAttr(key, value)\n if (kv !== undefined) attrs.push(kv)\n }\n\n // Always emit the gate state so operators can see it in the UI without\n // needing to grep the original config.\n attrs.push(bool(\"latitude.captured.content\", options.allowConversationAccess))\n // Mirror duration into the canonical name as well, when present.\n if (span.endMs !== undefined) {\n attrs.push(int(\"openclaw.duration_ms.computed\", Math.max(0, span.endMs - span.startMs)))\n }\n const redactedAttrs = redactAttributes(attrs, options.redact)\n\n const statusCode = span.outcome === \"error\" ? 2 : 1\n return {\n traceId: span.traceId,\n spanId: span.spanId,\n parentSpanId: span.parentSpanId,\n name: span.name,\n // OTel SpanKind: 1 = INTERNAL. None of agent/model_call/tool_call/\n // compaction/subagent map cleanly to CLIENT/SERVER/PRODUCER/CONSUMER —\n // OpenClaw is the source-of-truth runtime for all of them.\n kind: 1,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: redactedAttrs,\n status: { code: statusCode },\n }\n}\n\nfunction encodeAttr(key: string, value: AttrValue): OtlpKeyValue | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value === \"string\") return str(key, value)\n if (typeof value === \"boolean\") return bool(key, value)\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? int(key, value) : { key, value: { doubleValue: value } }\n }\n // Arrays + objects → JSON string. The Latitude UI parses the gen_ai.* keys\n // as JSON; anything else lands as opaque string and is queryable as a\n // contains-substring filter.\n return str(key, safeJson(value))\n}\n\n// ─── Resource + helper attribute encoders ──────────────────────────────────\n\nfunction resourceAttrs(): OtlpKeyValue[] {\n return [\n str(\"service.name\", \"openclaw\"),\n str(\"service.version\", SCOPE_VERSION),\n str(\"host.name\", hostname()),\n str(\"host.arch\", arch()),\n str(\"os.type\", platform()),\n str(\"os.version\", release()),\n ]\n}\n\nfunction str(key: string, value: string): OtlpKeyValue {\n return { key, value: { stringValue: value } }\n}\n\nfunction int(key: string, value: number): OtlpKeyValue {\n return { key, value: { intValue: String(Math.trunc(value)) } }\n}\n\nfunction bool(key: string, value: boolean): OtlpKeyValue {\n return { key, value: { boolValue: value } }\n}\n\nfunction msToNs(ms: number): string {\n return (BigInt(Math.trunc(ms)) * 1_000_000n).toString()\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n","/**\n * Normalizes provider-specific message shapes (Anthropic, OpenAI, pi-ai) into\n * the parts-based GenAI format Latitude's parser expects:\n *\n * { role: \"system\" | \"user\" | \"assistant\" | \"tool\", parts: MessagePart[] }\n *\n * Downstream consumers cast `gen_ai.input.messages` and `gen_ai.output.messages`\n * to `GenAIMessage[]` and read `message.parts` directly (e.g. for search\n * indexing). Without normalisation those casts produce objects without\n * `parts`, breaking rendering.\n *\n * The shapes we need to handle:\n *\n * - Anthropic: `{role, content: string}` or `{role, content: ContentBlock[]}`\n * with blocks `{type: \"text\", text}`, `{type: \"tool_use\", id, name, input}`,\n * `{type: \"tool_result\", tool_use_id, content}`, `{type: \"image\", source}`,\n * `{type: \"thinking\", thinking}`.\n * - OpenAI: `{role, content: string}` or with `tool_calls` field.\n * - pi-ai (OpenClaw's wrapper) — superset of the above.\n * - Already-normalized parts-shape messages — passed through unchanged.\n *\n * Anything we don't recognize falls through to a JSON-stringified text part\n * so nothing is silently dropped.\n */\n\nexport interface MessagePart {\n type: string\n content?: string\n // Tool call (assistant invokes a tool)\n id?: string\n name?: string\n arguments?: unknown\n // Tool response (tool replies)\n response?: unknown\n // Image / multimodal\n modality?: string\n uri?: string\n [key: string]: unknown\n}\n\nexport type MessageRole = \"system\" | \"user\" | \"assistant\" | \"tool\"\n\nexport interface Message {\n role: MessageRole\n parts: MessagePart[]\n}\n\nconst ALLOWED_ROLES: ReadonlySet<MessageRole> = new Set([\"system\", \"user\", \"assistant\", \"tool\"])\n\n/**\n * Normalize a single message of any of the provider shapes we know about.\n * Returns `undefined` for non-objects so the caller can skip them.\n */\nexport function normalizeMessage(raw: unknown): Message | undefined {\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n const role = coerceRole(obj.role)\n\n // Pre-normalized: already has a parts array.\n if (Array.isArray(obj.parts)) {\n const parts: MessagePart[] = []\n for (const p of obj.parts) {\n if (p && typeof p === \"object\") parts.push(p as MessagePart)\n }\n return { role, parts: parts.length > 0 ? parts : [{ type: \"text\", content: safeJson(raw) }] }\n }\n\n const content = obj.content ?? obj.text ?? obj.message\n\n // OpenAI tool message: `{role: \"tool\", tool_call_id, content}` — handle\n // before the generic string-content branch so we emit a tool_call_response\n // part rather than a plain text part.\n if (role === \"tool\" && obj.tool_call_id !== undefined) {\n return {\n role,\n parts: [\n {\n type: \"tool_call_response\",\n id: typeof obj.tool_call_id === \"string\" ? obj.tool_call_id : \"\",\n response: content ?? safeJson(obj),\n },\n ],\n }\n }\n\n if (typeof content === \"string\") {\n const parts: MessagePart[] = [{ type: \"text\", content }]\n // OpenAI assistant messages may have tool_calls alongside string content.\n appendToolCalls(parts, obj.tool_calls)\n return { role, parts }\n }\n\n if (Array.isArray(content)) {\n const parts: MessagePart[] = []\n for (const block of content) {\n const part = normalizeBlock(block)\n if (part) parts.push(part)\n }\n appendToolCalls(parts, obj.tool_calls)\n if (parts.length === 0) parts.push({ type: \"text\", content: safeJson(content) })\n return { role, parts }\n }\n\n // Unknown shape — dump as JSON so nothing is silently dropped.\n return { role, parts: [{ type: \"text\", content: safeJson(raw) }] }\n}\n\n/** Normalize an array of provider messages. */\nexport function normalizeMessages(raw: unknown[]): Message[] {\n const out: Message[] = []\n for (const m of raw) {\n const norm = normalizeMessage(m)\n if (norm) out.push(norm)\n }\n return out\n}\n\n/** Build a single user message from a string prompt. */\nexport function userMessageFromPrompt(prompt: string): Message {\n return { role: \"user\", parts: [{ type: \"text\", content: prompt }] }\n}\n\n/** Build a single assistant message from `assistantTexts` + `lastAssistant` fallback. */\nexport function assistantMessageFromOutput(assistantTexts: string[], lastAssistant: unknown): Message {\n if (lastAssistant !== undefined) {\n const norm = normalizeMessage(lastAssistant)\n if (norm) return { ...norm, role: \"assistant\" }\n }\n const parts: MessagePart[] = []\n for (const text of assistantTexts) {\n if (text.length > 0) parts.push({ type: \"text\", content: text })\n }\n if (parts.length === 0) parts.push({ type: \"text\", content: \"\" })\n return { role: \"assistant\", parts }\n}\n\n/**\n * Wrap a system prompt string into the parts-array shape expected for\n * `gen_ai.system_instructions`. Empty string in → single empty text part out\n * (still a valid array, never `undefined`).\n */\nexport function systemInstructionsParts(prompt: string): MessagePart[] {\n return [{ type: \"text\", content: prompt }]\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction coerceRole(raw: unknown): MessageRole {\n if (typeof raw !== \"string\") return \"user\"\n return ALLOWED_ROLES.has(raw as MessageRole) ? (raw as MessageRole) : \"user\"\n}\n\nfunction normalizeBlock(raw: unknown): MessagePart | undefined {\n if (typeof raw === \"string\") return { type: \"text\", content: raw }\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n\n // Already a part.\n if (typeof obj.type === \"string\" && (typeof obj.content === \"string\" || obj.content === undefined)) {\n // If it carries our recognized shape (text / tool_call / tool_call_response /\n // uri), pass through. Otherwise fall through to type-specific normalization.\n if (obj.type === \"text\" && typeof obj.content === \"string\") {\n return { type: \"text\", content: obj.content }\n }\n }\n\n const type = typeof obj.type === \"string\" ? obj.type : \"text\"\n\n if (type === \"text\" && typeof obj.text === \"string\") {\n return { type: \"text\", content: obj.text }\n }\n if (type === \"tool_use\") {\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.input ?? {},\n }\n }\n if (type === \"tool_call\") {\n // Already-normalized tool_call part — pass through.\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.arguments ?? obj.input ?? {},\n }\n }\n if (type === \"tool_result\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.tool_use_id === \"string\" ? obj.tool_use_id : \"\",\n response: obj.content ?? \"\",\n }\n }\n if (type === \"tool_call_response\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n response: obj.response ?? \"\",\n }\n }\n if (type === \"thinking\" && typeof obj.thinking === \"string\") {\n return { type: \"reasoning\", content: obj.thinking }\n }\n if (type === \"reasoning\" && typeof obj.content === \"string\") {\n return { type: \"reasoning\", content: obj.content }\n }\n if (type === \"image\" && obj.source && typeof obj.source === \"object\") {\n const src = obj.source as { media_type?: string; data?: string; url?: string }\n const uri = src.url ?? (src.data ? `data:${src.media_type ?? \"image/unknown\"};base64,${src.data}` : \"\")\n if (uri) return { type: \"uri\", modality: \"image\", uri }\n }\n\n // Unknown block type — stringify so nothing is silently dropped.\n return { type, content: safeJson(raw) }\n}\n\n/**\n * OpenAI assistant messages put tool calls in a separate `tool_calls` array\n * alongside string content. Append them as parts so the trace shows what the\n * model emitted in that turn.\n */\nfunction appendToolCalls(parts: MessagePart[], raw: unknown): void {\n if (!Array.isArray(raw)) return\n for (const tc of raw) {\n if (!tc || typeof tc !== \"object\") continue\n const t = tc as Record<string, unknown>\n const fn = t.function as { name?: string; arguments?: string | Record<string, unknown> } | undefined\n let parsedArgs: unknown = fn?.arguments\n if (typeof parsedArgs === \"string\") {\n try {\n parsedArgs = JSON.parse(parsedArgs)\n } catch {\n // leave as string\n }\n }\n parts.push({\n type: \"tool_call\",\n id: typeof t.id === \"string\" ? t.id : \"\",\n name: fn?.name ?? \"\",\n arguments: parsedArgs ?? {},\n })\n }\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n","import { createHash, randomUUID } from \"node:crypto\"\nimport {\n assistantMessageFromOutput,\n type Message,\n normalizeMessages,\n systemInstructionsParts,\n userMessageFromPrompt,\n} from \"./messages.ts\"\nimport type {\n OpenClawAfterCompactionEvent,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeAgentStartEvent,\n OpenClawBeforeCompactionEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawLlmUsage,\n OpenClawModelCallEndedEvent,\n OpenClawModelCallStartedEvent,\n OpenClawSubagentEndedEvent,\n OpenClawSubagentSpawnedEvent,\n} from \"./types.ts\"\n\n/**\n * Builds the per-trace span tree for an OpenClaw agent run from the granular\n * paired hooks. Replaces the older `turn-builder.ts` model that collapsed the\n * whole attempt into a single `llm_request` span — that shape was wrong on\n * two counts: `llm_input` / `llm_output` fire ONCE per attempt (not per\n * generation), and an attempt is a sequence of generations interleaved with\n * tool executions.\n *\n * Span set this builder produces:\n *\n * agent (root)\n * ├─ compaction (0..1, rare)\n * ├─ model_call (1..N, one per provider API call)\n * ├─ tool_call: ... (interleaved between model_calls; siblings of agent)\n * ├─ subagent (0..N; child agent runs nest INSIDE these via\n * │ └─ agent ... cross-runId trace propagation)\n * └─ model_call (final)\n *\n * Tool spans are siblings of `agent`, not children of `model_call`, because\n * tools run BETWEEN generations — not during them. Nesting under model_call\n * would falsely imply concurrency.\n *\n * `llm_input` / `llm_output` are NOT span boundaries here. They're data-only\n * feeds that enrich the parent `agent` span (full message history, output\n * messages, aggregate token usage).\n */\n\n// ─── Span record shapes ─────────────────────────────────────────────────────\n\nexport interface SpanRecord {\n /** Stable id for the span (16 hex chars). */\n spanId: string\n /** Span tree id (32 hex chars). */\n traceId: string\n /** Empty string for root agent spans, parent's spanId otherwise. */\n parentSpanId: string\n /** OpenClaw event noun (`agent` / `model_call` / `tool_call` / `compaction` / `subagent`). */\n name: string\n startMs: number\n endMs: number | undefined\n /** Free-form attribute bag — flattened to OTLP key/value at emit time. */\n attrs: Record<string, AttrValue>\n /** Status — set at close from the event payload's outcome/error. */\n outcome?: \"ok\" | \"error\"\n errorMessage?: string | undefined\n}\n\nexport type AttrValue = string | number | boolean | unknown[] | Record<string, unknown> | undefined\n\n// One entry per simple `prefix.field` attribute the builder records on a span.\ntype AttrInput = Record<string, AttrValue>\n\n// ─── Per-run state ──────────────────────────────────────────────────────────\n\ninterface RunState {\n /** Trace root span (the `agent`). */\n agent: SpanRecord\n /**\n * Working snapshot of conversation history in the parts-based GenAI shape\n * Latitude's parser expects. Provider-specific shapes from `llm_input` get\n * normalized once on entry; tool_call / tool_call_response parts appended\n * during the run are already in the right shape.\n */\n history: Message[]\n /** Open per-call spans, keyed on the OpenClaw `callId` from `model_call_started`. */\n openModelCalls: Map<string, SpanRecord>\n /** Open tool spans, keyed on `toolCallId`. */\n openToolCalls: Map<string, SpanRecord>\n /** Open compaction span (at most one in flight). */\n openCompaction: SpanRecord | undefined\n /** All closed spans for this run, ready to emit on agent_end. */\n closed: SpanRecord[]\n /** Any subagent spans we OPENED inside this run, keyed by child runId so we\n * can close them when the child's `subagent_ended` arrives. */\n childSubagentSpans: Map<string, SpanRecord>\n}\n\n/**\n * When a parent's `subagent_spawned` fires we register a link from the\n * child's runId → the parent's traceId + the subagent span's id. Then when the\n * child's `before_agent_start` fires, we use those values so the child's\n * entire span subtree lands inside the parent's trace. Outlives the parent's\n * RunState because the child's `agent_end` may arrive after the parent's.\n *\n * `createdAt` is used by the `evictStaleSubagentLinks` sweep to drop entries\n * whose child runs never reached `agent_end` (gateway crash mid-spawn,\n * plugin reload, etc.). Without the sweep the map grows unbounded over the\n * lifetime of a long-running OpenClaw process.\n */\ninterface SubagentLink {\n traceId: string\n /** Span id of the parent's `subagent` span — used as parentSpanId for the child's root. */\n subagentSpanId: string\n createdAt: number\n}\n\nconst SUBAGENT_LINK_TTL_MS = 60 * 60 * 1000 // 1 hour\nconst SUBAGENT_LINK_MAX = 1000\n\nexport interface BuildResult {\n /** Run id this batch belongs to. */\n runId: string\n /** All spans ready to be exported (agent + everything beneath it). */\n spans: SpanRecord[]\n}\n\n// ─── Builder ────────────────────────────────────────────────────────────────\n\nexport class SpanBuilder {\n private readonly runs = new Map<string, RunState>()\n private readonly subagentLinks = new Map<string, SubagentLink>()\n\n inflightCount(): number {\n return this.runs.size\n }\n\n /**\n * Open the root `agent` span. If the runId was previously registered as a\n * subagent's child, propagate the parent's traceId and parent the new span\n * under the parent's `subagent` span — so the entire subagent's work nests\n * inside the parent's trace as one waterfall.\n */\n onBeforeAgentStart(evt: OpenClawBeforeAgentStartEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n if (this.runs.has(runId)) return // defensive — already open\n\n const link = this.subagentLinks.get(runId)\n const traceId = link?.traceId ?? hashHex(runId, 32)\n const parentSpanId = link?.subagentSpanId ?? \"\"\n\n const agent: SpanRecord = {\n spanId: hashHex(`${traceId}:${runId}:agent`, 16),\n traceId,\n parentSpanId,\n name: \"agent\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...flattenCtx(ctx),\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.run.id\": runId,\n // before_agent_start payload — gated content fields go via `gated.*`\n // attribute keys so the OTLP layer can scrub them without a parallel\n // boolean check. Messages get normalized to parts-shape; `prompt` is\n // a plain string (for the user-prompt convenience attribute).\n \"before_agent_start.prompt:gated\": evt.prompt,\n \"before_agent_start.messages:gated\": evt.messages ? normalizeMessages(evt.messages) : undefined,\n },\n }\n\n this.runs.set(runId, {\n agent,\n history: [],\n openModelCalls: new Map(),\n openToolCalls: new Map(),\n openCompaction: undefined,\n closed: [],\n childSubagentSpans: new Map(),\n })\n }\n\n /**\n * Enrich the open `agent` span with content + identity from the LLM input.\n * Also seeds the rolling history snapshot used by per-call `model_call`\n * input attributes.\n *\n * Provider-specific message shapes get normalized into the parts-based\n * GenAI format here — that's the contract Latitude's downstream parser\n * expects on `gen_ai.input.messages` and `gen_ai.system_instructions`.\n */\n onLlmInput(evt: OpenClawLlmInputEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(ctx.runId ?? evt.runId)\n if (!run) return\n\n const normalizedHistory = normalizeMessages(evt.historyMessages)\n const inputMessages: Message[] = [...normalizedHistory]\n if (evt.prompt) inputMessages.push(userMessageFromPrompt(evt.prompt))\n\n Object.assign(run.agent.attrs, {\n \"gen_ai.system_instructions:gated\": evt.systemPrompt ? systemInstructionsParts(evt.systemPrompt) : undefined,\n \"user_prompt:gated\": evt.prompt,\n \"gen_ai.input.messages:gated\": inputMessages,\n \"openclaw.images.count\": evt.imagesCount,\n \"gen_ai.request.model\": evt.model,\n \"gen_ai.system\": evt.provider,\n \"openclaw.provider\": evt.provider,\n })\n // Seed the rolling history with the normalized form. The per-call\n // model_call_started will copy the snapshot at the time it fires;\n // subsequent before_tool_call / after_tool_call events append to the\n // same array (already in parts shape) so the next model_call captures\n // the post-tool state.\n run.history = inputMessages\n }\n\n /**\n * Enrich the agent span with attempt-aggregate output + token usage.\n * (Per-call usage isn't surfaced by OpenClaw today — see PR #2986.)\n */\n onLlmOutput(evt: OpenClawLlmOutputEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(ctx.runId ?? evt.runId)\n if (!run) return\n const assistantMessage = assistantMessageFromOutput(evt.assistantTexts, evt.lastAssistant)\n Object.assign(run.agent.attrs, {\n \"gen_ai.output.messages:gated\": [assistantMessage],\n \"openclaw.resolved.ref\": evt.resolvedRef,\n \"openclaw.harness.id\": evt.harnessId,\n \"gen_ai.response.model\": evt.model,\n ...usageAttrs(evt.usage),\n })\n }\n\n onModelCallStarted(evt: OpenClawModelCallStartedEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(evt.runId)\n if (!run) return\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:model_call:${evt.callId}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: \"model_call\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.run.id\": evt.runId,\n \"openclaw.call.id\": evt.callId,\n \"gen_ai.system\": evt.provider,\n \"openclaw.provider\": evt.provider,\n \"gen_ai.request.model\": evt.model,\n \"openclaw.api\": evt.api,\n \"openclaw.transport\": evt.transport,\n // Snapshot the rolling history at the moment this generation starts.\n // The model saw exactly this state. Gated.\n \"gen_ai.input.messages:gated\": [...run.history],\n },\n }\n run.openModelCalls.set(evt.callId, span)\n }\n\n onModelCallEnded(evt: OpenClawModelCallEndedEvent, _ctx: OpenClawAgentContext): void {\n const run = this.runs.get(evt.runId)\n if (!run) return\n const span = run.openModelCalls.get(evt.callId)\n if (!span) return\n span.endMs = Date.now()\n span.outcome = evt.outcome === \"completed\" ? \"ok\" : \"error\"\n span.errorMessage = evt.errorCategory\n Object.assign(span.attrs, {\n \"openclaw.duration_ms\": evt.durationMs,\n \"openclaw.outcome\": evt.outcome,\n \"openclaw.error.category\": evt.errorCategory,\n \"openclaw.failure.kind\": evt.failureKind,\n \"openclaw.request.payload_bytes\": evt.requestPayloadBytes,\n \"openclaw.response.stream_bytes\": evt.responseStreamBytes,\n \"openclaw.ttfb_ms\": evt.timeToFirstByteMs,\n \"openclaw.upstream.request_id_hash\": evt.upstreamRequestIdHash,\n })\n run.openModelCalls.delete(evt.callId)\n run.closed.push(span)\n }\n\n /**\n * Open a `tool_call` span as a sibling of the agent span. Also append a\n * synthetic assistant `tool_call` part to the rolling history so the NEXT\n * model_call's input snapshot reflects what the model emitted.\n *\n * IMPORTANT: this runs as a `runModifyingHook` in OpenClaw — returning\n * anything other than `undefined`/falsy from this handler blocks the tool.\n * The plugin-side handler enforces a void return; this method's signature\n * already returns `void`.\n */\n onBeforeToolCall(evt: OpenClawBeforeToolCallEvent, ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n\n const toolCallId = evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:tool_call:${toolCallId}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: `tool_call:${evt.toolName}`,\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.run.id\": evt.runId,\n \"gen_ai.tool.name\": evt.toolName,\n \"gen_ai.tool.call.id\": toolCallId,\n \"gen_ai.tool.call.arguments:gated\": evt.params,\n },\n }\n run.openToolCalls.set(toolCallId, span)\n\n // Append an assistant tool_call part to the rolling history so the next\n // model_call captures it.\n run.history.push({\n role: \"assistant\",\n parts: [{ type: \"tool_call\", id: toolCallId, name: evt.toolName, arguments: evt.params }],\n })\n }\n\n onAfterToolCall(evt: OpenClawAfterToolCallEvent, _ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n\n // Match priority:\n // 1. Direct id lookup — the happy path.\n // 2. Name-match fallback — if `evt.toolCallId` is missing OR refers to\n // an id we didn't open (e.g. before_tool_call elided it and we\n // synthesised one, then after_tool_call provided the real one).\n //\n // Without the id-mismatch fallback, the open span would never close\n // and would get force-closed as `abandoned` at agent_end.\n let resolvedId: string | undefined =\n evt.toolCallId && run.openToolCalls.has(evt.toolCallId) ? evt.toolCallId : undefined\n if (!resolvedId) resolvedId = this.findOpenToolCallByName(run, evt.toolName)\n if (!resolvedId) return\n const span = run.openToolCalls.get(resolvedId)\n if (!span) return\n const toolCallId: string = resolvedId\n\n span.endMs = Date.now()\n const isError = Boolean(evt.error)\n span.outcome = isError ? \"error\" : \"ok\"\n span.errorMessage = evt.error\n Object.assign(span.attrs, {\n \"gen_ai.tool.call.result:gated\": evt.result,\n \"openclaw.error.message:gated\": evt.error,\n \"openclaw.duration_ms\": evt.durationMs,\n })\n run.openToolCalls.delete(toolCallId)\n run.closed.push(span)\n\n // Append the tool response to the rolling history so the next\n // model_call's input snapshot includes it.\n run.history.push({\n role: \"tool\",\n parts: [{ type: \"tool_call_response\", id: toolCallId, response: evt.result ?? evt.error ?? \"\" }],\n })\n }\n\n onBeforeCompaction(evt: OpenClawBeforeCompactionEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n const run = this.runs.get(runId)\n if (!run) return\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:compaction:${run.closed.length}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: \"compaction\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.run.id\": runId,\n \"openclaw.compaction.message_count.before\": evt.messageCount,\n \"openclaw.compaction.session_file\": evt.sessionFile,\n \"before_compaction.messages:gated\": evt.messages ? normalizeMessages(evt.messages) : undefined,\n },\n }\n run.openCompaction = span\n }\n\n onAfterCompaction(evt: OpenClawAfterCompactionEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n const run = this.runs.get(runId)\n if (!run?.openCompaction) return\n const span = run.openCompaction\n span.endMs = Date.now()\n span.outcome = \"ok\"\n Object.assign(span.attrs, {\n \"openclaw.compaction.message_count.after\": evt.messageCount,\n \"openclaw.compaction.compacted_count\": evt.compactedCount,\n \"openclaw.compaction.token_count\": evt.tokenCount,\n })\n run.openCompaction = undefined\n run.closed.push(span)\n }\n\n /**\n * Open a `subagent` span on the parent run AND register the cross-run link\n * so the child's `before_agent_start` can find us.\n */\n onSubagentSpawned(evt: OpenClawSubagentSpawnedEvent, ctx: OpenClawAgentContext): void {\n const parentRunId = ctx.runId\n if (!parentRunId) return\n const parent = this.runs.get(parentRunId)\n if (!parent) return\n\n const span: SpanRecord = {\n spanId: hashHex(`${parent.agent.traceId}:subagent:${evt.runId}`, 16),\n traceId: parent.agent.traceId,\n parentSpanId: parent.agent.spanId,\n name: \"subagent\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n // The subagent span lives inside the parent's trace — tags +\n // metadata reflect the parent's ctx (which agent/channel/trigger\n // is doing the spawning), not the child's. The child's `agent` span\n // (opened later by the child run's before_agent_start) carries\n // its OWN ctx.\n ...latitudeAttrs(ctx),\n ...sessionAttrs(ctx),\n \"openclaw.parent.run.id\": parentRunId,\n \"openclaw.run.id\": evt.runId,\n \"openclaw.subagent.child_session_key\": evt.childSessionKey,\n \"openclaw.subagent.agent_id\": evt.agentId,\n \"openclaw.subagent.label\": evt.label,\n \"openclaw.subagent.mode\": evt.mode,\n \"openclaw.subagent.thread_requested\": evt.threadRequested,\n \"openclaw.subagent.requester.channel\": evt.requester?.channel,\n \"openclaw.subagent.requester.account_id\": evt.requester?.accountId,\n \"openclaw.subagent.requester.to\": evt.requester?.to,\n \"openclaw.subagent.requester.thread_id\":\n typeof evt.requester?.threadId === \"string\" || typeof evt.requester?.threadId === \"number\"\n ? String(evt.requester.threadId)\n : undefined,\n },\n }\n parent.childSubagentSpans.set(evt.runId, span)\n this.evictStaleSubagentLinks()\n this.subagentLinks.set(evt.runId, {\n traceId: parent.agent.traceId,\n subagentSpanId: span.spanId,\n createdAt: Date.now(),\n })\n }\n\n onSubagentEnded(evt: OpenClawSubagentEndedEvent, ctx: OpenClawAgentContext): void {\n const parentRunId = ctx.runId\n if (!parentRunId) return\n const parent = this.runs.get(parentRunId)\n if (!parent) return\n const childRunId = evt.runId\n if (!childRunId) return\n const span = parent.childSubagentSpans.get(childRunId)\n if (!span) return\n\n span.endMs = Date.now()\n const isError = evt.outcome === \"error\" || Boolean(evt.error)\n span.outcome = isError ? \"error\" : \"ok\"\n span.errorMessage = evt.error\n Object.assign(span.attrs, {\n \"openclaw.subagent.target_session_key\": evt.targetSessionKey,\n \"openclaw.subagent.target_kind\": evt.targetKind,\n \"openclaw.subagent.reason\": evt.reason,\n \"openclaw.subagent.outcome\": evt.outcome,\n \"openclaw.subagent.send_farewell\": evt.sendFarewell,\n \"openclaw.subagent.account_id\": evt.accountId,\n })\n parent.childSubagentSpans.delete(childRunId)\n parent.closed.push(span)\n // Don't delete the subagent link yet — the child's `agent_end` may still\n // be in flight. We clean it up when the child's agent_end fires.\n }\n\n /**\n * Close out the run: finish the agent span, abandon any still-open\n * model_calls / tool_calls / compactions, and return everything ready to\n * emit. Removes the subagent link if this was a child run.\n */\n onAgentEnd(evt: OpenClawAgentEndEvent, ctx: OpenClawAgentContext): BuildResult | undefined {\n const runId = ctx.runId\n if (!runId) return undefined\n const run = this.runs.get(runId)\n if (!run) {\n // Child agent run that closed without ever opening — drop the link\n // entry so the map doesn't grow unbounded.\n this.subagentLinks.delete(runId)\n return undefined\n }\n\n const now = Date.now()\n run.agent.endMs = now\n run.agent.outcome = evt.success ? \"ok\" : \"error\"\n run.agent.errorMessage = evt.error\n Object.assign(run.agent.attrs, {\n \"openclaw.duration_ms\": evt.durationMs,\n \"openclaw.run.success\": evt.success,\n \"openclaw.error.message:gated\": evt.error,\n \"agent_end.messages:gated\": normalizeMessages(evt.messages),\n })\n\n // Anything still open at agent_end didn't get a proper close event.\n // Mark them abandoned and force-close so they show up in the trace\n // rather than vanish silently.\n for (const span of run.openModelCalls.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n for (const span of run.openToolCalls.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n if (run.openCompaction) {\n run.openCompaction.endMs = now\n run.openCompaction.outcome = \"error\"\n run.openCompaction.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(run.openCompaction)\n }\n for (const span of run.childSubagentSpans.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.subagent.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n\n const spans = [run.agent, ...run.closed]\n this.runs.delete(runId)\n this.subagentLinks.delete(runId)\n return { runId, spans }\n }\n\n /** Drop a run without emitting — used on errors from the emit path. */\n abandon(runId: string): void {\n this.runs.delete(runId)\n this.subagentLinks.delete(runId)\n }\n\n /** Test-only: how many cross-run subagent links we're holding. */\n subagentLinkCount(): number {\n return this.subagentLinks.size\n }\n\n /**\n * Drop any subagent links whose child run never reached `agent_end`. Called\n * before every `subagent_spawned` insert so the map stays bounded even when\n * children crash mid-spawn or the plugin reloads.\n *\n * Two passes: TTL eviction (anything older than `SUBAGENT_LINK_TTL_MS`),\n * then a hard size cap (when we're past `SUBAGENT_LINK_MAX`, drop the\n * oldest until we're under).\n */\n private evictStaleSubagentLinks(): void {\n const now = Date.now()\n for (const [runId, link] of this.subagentLinks) {\n if (now - link.createdAt > SUBAGENT_LINK_TTL_MS) {\n this.subagentLinks.delete(runId)\n }\n }\n if (this.subagentLinks.size <= SUBAGENT_LINK_MAX) return\n const sorted = Array.from(this.subagentLinks.entries()).sort((a, b) => a[1].createdAt - b[1].createdAt)\n const toRemove = this.subagentLinks.size - SUBAGENT_LINK_MAX\n for (let i = 0; i < toRemove; i++) {\n const entry = sorted[i]\n if (entry) this.subagentLinks.delete(entry[0])\n }\n }\n\n private findOpenToolCallByName(run: RunState, toolName: string): string | undefined {\n // Defensive: OpenClaw versions that elide toolCallId on after_tool_call\n // can be matched by name + still-open status. When multiple in-flight\n // tool calls share a name, prefer the MOST RECENTLY opened — Maps\n // preserve insertion order, so iterating in reverse picks the latest.\n // (LIFO matches typical agent runtimes that issue tools sequentially.)\n const target = `tool_call:${toolName}`\n const entries = Array.from(run.openToolCalls.entries())\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i]\n if (!entry) continue\n const [id, span] = entry\n if (span.name === target) return id\n }\n return undefined\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction flattenCtx(ctx: OpenClawAgentContext): AttrInput {\n return {\n \"openclaw.run.id\": ctx.runId,\n \"openclaw.session.id\": ctx.sessionId,\n \"openclaw.session.key\": ctx.sessionKey,\n \"openclaw.agent.id\": ctx.agentId,\n \"openclaw.agent.name\": ctx.agentId,\n \"openclaw.workspace.dir\": ctx.workspaceDir,\n \"openclaw.message.provider\": ctx.messageProvider,\n \"openclaw.trigger\": ctx.trigger,\n \"openclaw.channel.id\": ctx.channelId,\n \"openclaw.cron.job.id\": ctx.jobId,\n \"openclaw.model.provider.id\": ctx.modelProviderId,\n \"openclaw.model.id\": ctx.modelId,\n }\n}\n\n/**\n * Mirror OpenClaw's session id onto the OTEL-standard keys Latitude's\n * resolver looks for. `gen_ai.session.id` and `session.id` are both in\n * `sessionIdCandidates` (domain/spans/src/otlp/resolvers/identity.ts), so\n * traces can be grouped by session in the Latitude UI without an\n * openclaw-specific code path. Emitted on every span, not just `agent`,\n * so child spans (model_call / tool_call / etc.) inherit the same grouping.\n */\nfunction sessionAttrs(ctx: OpenClawAgentContext): AttrInput {\n if (!ctx.sessionId) return {}\n return {\n \"session.id\": ctx.sessionId,\n \"gen_ai.session.id\": ctx.sessionId,\n }\n}\n\n/**\n * Build `latitude.tags` and `latitude.metadata` attrs from the hook context.\n * The OTLP encoder JSON-stringifies arrays/objects, which is the encoding\n * Latitude's resolver expects:\n *\n * - `latitude.tags` is a JSON-encoded string array (`fromJsonStringArray`\n * in domain/spans/src/otlp/resolvers/enrichment.ts).\n * - `latitude.metadata` is a JSON-encoded string object (`fromJsonString`).\n *\n * Tags = the agent id, the channel id, and the trigger. When trigger is\n * `cron`, the tag becomes `cron:<jobId>` so dashboards can pivot on the\n * specific cron job. Each tag is conditionally included so absent ctx\n * fields don't produce empty entries.\n *\n * Metadata = every ctx field that's set, namespaced under `openclaw.*` so\n * it can't collide with metadata keys other plugins might emit.\n */\nfunction latitudeAttrs(ctx: OpenClawAgentContext): AttrInput {\n const tags: string[] = []\n if (ctx.agentId) tags.push(ctx.agentId)\n if (ctx.channelId) tags.push(ctx.channelId)\n if (ctx.trigger) {\n tags.push(ctx.trigger === \"cron\" && ctx.jobId ? `cron:${ctx.jobId}` : ctx.trigger)\n }\n\n const metadata: Record<string, string> = {}\n if (ctx.runId) metadata[\"openclaw.run.id\"] = ctx.runId\n if (ctx.sessionId) metadata[\"openclaw.session.id\"] = ctx.sessionId\n if (ctx.sessionKey) metadata[\"openclaw.session.key\"] = ctx.sessionKey\n if (ctx.agentId) metadata[\"openclaw.agent.id\"] = ctx.agentId\n if (ctx.workspaceDir) metadata[\"openclaw.workspace.dir\"] = ctx.workspaceDir\n if (ctx.channelId) metadata[\"openclaw.channel.id\"] = ctx.channelId\n if (ctx.messageProvider) metadata[\"openclaw.message.provider\"] = ctx.messageProvider\n if (ctx.trigger) metadata[\"openclaw.trigger\"] = ctx.trigger\n if (ctx.jobId) metadata[\"openclaw.cron.job.id\"] = ctx.jobId\n if (ctx.modelProviderId) metadata[\"openclaw.model.provider.id\"] = ctx.modelProviderId\n if (ctx.modelId) metadata[\"openclaw.model.id\"] = ctx.modelId\n\n return {\n \"latitude.tags\": tags.length > 0 ? tags : undefined,\n \"latitude.metadata\": Object.keys(metadata).length > 0 ? metadata : undefined,\n }\n}\n\nfunction usageAttrs(usage: OpenClawLlmUsage | undefined): AttrInput {\n if (!usage) return {}\n return {\n \"gen_ai.usage.input_tokens\": usage.input,\n \"gen_ai.usage.output_tokens\": usage.output,\n \"gen_ai.usage.cache_read_input_tokens\": usage.cacheRead,\n \"gen_ai.usage.cache_creation_input_tokens\": usage.cacheWrite,\n \"gen_ai.usage.total_tokens\": usage.total,\n }\n}\n\nfunction hashHex(input: string, length: number): string {\n return createHash(\"sha256\").update(input).digest(\"hex\").slice(0, length)\n}\n","import { postTraces } from \"./client.ts\"\nimport { type Config, loadConfig } from \"./config.ts\"\nimport { createLogger, type Logger } from \"./logger.ts\"\nimport { buildOtlpRequest } from \"./otlp.ts\"\nimport { type BuildResult, SpanBuilder } from \"./span-builder.ts\"\nimport type {\n OpenClawAfterCompactionEvent,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeAgentStartEvent,\n OpenClawBeforeCompactionEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawModelCallEndedEvent,\n OpenClawModelCallStartedEvent,\n OpenClawSubagentEndedEvent,\n OpenClawSubagentSpawnedEvent,\n} from \"./types.ts\"\n\n/**\n * Minimal structural type for OpenClaw's plugin API — only the fields we\n * touch. We avoid importing from `openclaw/plugin-sdk` so the package stays\n * usable when OpenClaw isn't installed (the CLI and tests don't need it),\n * and so we're robust to small signature changes across OpenClaw versions.\n *\n * `pluginConfig` is the user's `plugins.entries[id].config` block — that's\n * the canonical place to read credentials and feature flags. The OpenClaw\n * plugin SDK also exposes the same value as `api.pluginConfig` on the\n * builder API; keep both names in sync if the upstream contract evolves.\n */\nexport interface OpenClawPluginApiLike {\n logger?: Logger\n pluginConfig?: Record<string, unknown>\n on: <K extends string>(\n hookName: K,\n handler: (event: unknown, ctx: unknown) => unknown,\n opts?: { priority?: number },\n ) => void\n}\n\nexport interface RegisterOptions {\n /** Override the config, mostly for tests. */\n config?: Config\n /** Override the logger. */\n logger?: Logger\n /**\n * Hook to observe the emitted run right before it's posted. Used by tests;\n * not a stable public API.\n */\n onEmit?: (result: BuildResult) => void\n}\n\n/**\n * Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls\n * this once at plugin activation; we wire up the granular paired hooks\n * (model_call_started/_ended, before_/after_tool_call, before_/after_compaction,\n * subagent_spawned/_ended, before_agent_start/agent_end) plus the\n * data-only feeds (llm_input/llm_output) that enrich the agent span.\n *\n * Every typed hook on OpenClaw's side fires fire-and-forget for non-modifying\n * hooks; before_tool_call is a `runModifyingHook` where returning anything\n * other than undefined blocks the tool call. Our handler returns nothing —\n * keep it that way.\n */\nexport default function registerLatitudePlugin(api: OpenClawPluginApiLike, opts: RegisterOptions = {}): void {\n // Source of truth: OpenClaw passes the user's `plugins.entries[id].config`\n // as `api.pluginConfig`. Env vars are a fallback so existing deploys with\n // LATITUDE_* exported in the gateway environment keep working.\n const config = opts.config ?? loadConfig(api.pluginConfig)\n const logger = opts.logger ?? createLogger(config.debug)\n\n if (!config.enabled) {\n if (config.apiKey === \"\") logger.debug(\"disabled: apiKey is empty (set plugins.entries[id].config.apiKey)\")\n if (config.project === \"\") logger.debug(\"disabled: project is empty (set plugins.entries[id].config.project)\")\n return\n }\n logger.debug(\n `enabled: project=${config.project} base=${config.baseUrl} allowConversationAccess=${config.allowConversationAccess}`,\n )\n\n const builder = new SpanBuilder()\n\n // Helper: wrap a void-returning hook handler with try/catch + cast.\n const wrap = <E>(\n name: string,\n fn: (evt: E, ctx: OpenClawAgentContext) => void,\n ): ((evt: unknown, ctx: unknown) => void) => {\n return (evt, ctx) => {\n try {\n fn(evt as E, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`${name} handler failed: ${String(err)}`)\n }\n }\n }\n\n // ─── Span boundaries ────────────────────────────────────────────────────\n\n api.on(\n \"before_agent_start\",\n wrap<OpenClawBeforeAgentStartEvent>(\"before_agent_start\", (evt, ctx) => {\n builder.onBeforeAgentStart(evt, ctx)\n }),\n )\n\n api.on(\n \"model_call_started\",\n wrap<OpenClawModelCallStartedEvent>(\"model_call_started\", (evt, ctx) => {\n builder.onModelCallStarted(evt, ctx)\n }),\n )\n api.on(\n \"model_call_ended\",\n wrap<OpenClawModelCallEndedEvent>(\"model_call_ended\", (evt, ctx) => {\n builder.onModelCallEnded(evt, ctx)\n }),\n )\n\n // before_tool_call is a `runModifyingHook` — returning {block: true} from\n // any plugin handler blocks the tool. We return nothing (void) so OpenClaw\n // dispatches normally. The `wrap` helper preserves that void return.\n api.on(\n \"before_tool_call\",\n wrap<OpenClawBeforeToolCallEvent>(\"before_tool_call\", (evt, ctx) => {\n builder.onBeforeToolCall(evt, ctx)\n }),\n )\n api.on(\n \"after_tool_call\",\n wrap<OpenClawAfterToolCallEvent>(\"after_tool_call\", (evt, ctx) => {\n builder.onAfterToolCall(evt, ctx)\n }),\n )\n\n api.on(\n \"before_compaction\",\n wrap<OpenClawBeforeCompactionEvent>(\"before_compaction\", (evt, ctx) => {\n builder.onBeforeCompaction(evt, ctx)\n }),\n )\n api.on(\n \"after_compaction\",\n wrap<OpenClawAfterCompactionEvent>(\"after_compaction\", (evt, ctx) => {\n builder.onAfterCompaction(evt, ctx)\n }),\n )\n\n api.on(\n \"subagent_spawned\",\n wrap<OpenClawSubagentSpawnedEvent>(\"subagent_spawned\", (evt, ctx) => {\n builder.onSubagentSpawned(evt, ctx)\n }),\n )\n api.on(\n \"subagent_ended\",\n wrap<OpenClawSubagentEndedEvent>(\"subagent_ended\", (evt, ctx) => {\n builder.onSubagentEnded(evt, ctx)\n }),\n )\n\n // ─── Data-only feeds ────────────────────────────────────────────────────\n // These DON'T open or close spans. They enrich the open `agent` span with\n // attempt-aggregate content + token usage, and seed the rolling history\n // snapshot used by per-call `model_call.input.messages`.\n\n api.on(\n \"llm_input\",\n wrap<OpenClawLlmInputEvent>(\"llm_input\", (evt, ctx) => {\n builder.onLlmInput(evt, ctx)\n }),\n )\n api.on(\n \"llm_output\",\n wrap<OpenClawLlmOutputEvent>(\"llm_output\", (evt, ctx) => {\n builder.onLlmOutput(evt, ctx)\n }),\n )\n\n // ─── Trace flush ────────────────────────────────────────────────────────\n //\n // Why we defer the finalize by one microtask tick instead of finalizing\n // synchronously inside the agent_end handler: OpenClaw 2026.4.26+ has TWO\n // hook fire-orders depending on which runtime the agent uses, and the\n // selection.runtime path (used by the codex / embedded ACPX agents) fires\n // events in this order:\n //\n // llm_input → ...model_calls / tool_calls... → agent_end → llm_output\n //\n // The cli-runner.runtime path (used by the claude-code agent) fires the\n // reverse — `llm_output` BEFORE `agent_end` — and only when the assistant\n // emitted a non-empty text part.\n //\n // If we finalize on agent_end synchronously, the run is deleted and the OTLP\n // batch is shipped before `llm_output` (under selection.runtime) gets a\n // chance to enrich the agent span with `gen_ai.output.messages`,\n // `gen_ai.response.model`, `openclaw.resolved.ref`, `openclaw.harness.id`,\n // and the entire `gen_ai.usage.*` block. The `onLlmOutput` handler then\n // bails on `if (!run) return` cleanly (no error, no warning), and every\n // attribute that lives on the `llm_output` event is silently dropped.\n //\n // Deferring with `queueMicrotask` is order-agnostic: in either path, both\n // hook handlers run synchronously in the current microtask round and write\n // to the still-alive run; the queued finalize fires after both have\n // completed and serializes a fully-enriched batch. Subagents go through\n // exactly the same `onAgentEnd` path so they benefit automatically.\n //\n // We don't use `setTimeout(0)` because the +1 macrotask of latency isn't\n // meaningful here, and `queueMicrotask` is more reliable on process exit\n // (microtasks drain before exit; macrotasks may not). If a future OpenClaw\n // ever introduces an `await` between `agent_end` dispatch and `llm_output`\n // dispatch, we'll need to switch to `setTimeout(0)`.\n api.on(\n \"agent_end\",\n wrap<OpenClawAgentEndEvent>(\"agent_end\", (evt, ctx) => {\n queueMicrotask(() => {\n try {\n const result = builder.onAgentEnd(evt, ctx)\n if (!result) {\n logger.debug(\"agent_end fired without a matching run in flight\")\n return\n }\n opts.onEmit?.(result)\n const payload = buildOtlpRequest(result, {\n allowConversationAccess: config.allowConversationAccess,\n redact: config.redact,\n })\n void postTraces({\n baseUrl: config.baseUrl,\n apiKey: config.apiKey,\n project: config.project,\n payload,\n logger,\n })\n } catch (err) {\n logger.warn(`agent_end finalize failed: ${String(err)}`)\n }\n })\n }),\n )\n}\n"],"mappings":";;;AAGA,eAAsB,WAAW,EAC/B,SACA,QACA,SACA,SACA,QACA,YAAY,OAQI;CAChB,MAAM,MAAM,GAAG,QAAQ,QAAQ,QAAQ,GAAG,CAAC;CAC3C,MAAM,WAAW,KAAK,UAAU,QAAQ;AACxC,QAAO,MAAM,QAAQ,IAAI,YAAY,QAAQ,IAAI,SAAS,OAAO,SAAS;CAE1E,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,QAAQ,iBAAiB,WAAW,OAAO,EAAE,UAAU;AAC7D,KAAI;EACF,MAAM,MAAM,MAAM,MAAM,KAAK;GAC3B,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,eAAe,UAAU;IACzB,sBAAsB;IACvB;GACD,MAAM;GACN,QAAQ,WAAW;GACpB,CAAC;AACF,MAAI,CAAC,IAAI,IAAI;GACX,MAAM,OAAO,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG;AAC7C,UAAO,KAAK,eAAe,IAAI,OAAO,IAAI,KAAK,MAAM,GAAG,IAAI,GAAG;QAE/D,QAAO,MAAM,eAAe,IAAI,SAAS;UAEpC,KAAK;AACZ,SAAO,KAAK,kBAAkB,OAAO,IAAI,GAAG;WACpC;AACR,eAAa,MAAM;;;;;ACrCvB,SAAgB,kBAAkB,OAA0C;AAC1E,KAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CAAE,QAAO,KAAA;CACxE,MAAM,MAAM;CACZ,MAAM,aAAa,gBAAgB,IAAI,WAAW;AAClD,KAAI,WAAW,WAAW,EAAG,QAAO,KAAA;AACpC,QAAO;EAAE;EAAY,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;EAAU;;AAGjF,SAAgB,iBAAiB,YAA4B,QAAkD;AAC7G,KAAI,CAAC,OAAQ,QAAO;CACpB,MAAM,WAAW,OAAO,WAAW,IAAI,UAAU,CAAC,QAAQ,YAAiD,CAAC,CAAC,QAAQ;AACrH,KAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAO,WAAW,KAAK,SACrB,SAAS,MAAM,YAAY,QAAQ,KAAK,IAAI,CAAC,GAAG,aAAa,KAAK,KAAK,OAAO,KAAK,GAAG,KACvF;;AAGH,SAAS,gBAAgB,OAA0B;AACjD,KAAI,CAAC,MAAM,QAAQ,MAAM,CAAE,QAAO,EAAE;AACpC,QAAO,MAAM,QAAQ,SAAyB,OAAO,SAAS,YAAY,KAAK,MAAM,KAAK,GAAG;;AAG/F,SAAS,UAAU,SAAyD;AAC1E,KAAI,QAAQ,WAAW,IAAI,IAAI,QAAQ,YAAY,IAAI,GAAG,GAAG;EAC3D,MAAM,MAAM,QAAQ,YAAY,IAAI;AACpC,MAAI;GACF,MAAM,QAAQ,IAAI,OAAO,QAAQ,MAAM,GAAG,IAAI,EAAE,QAAQ,MAAM,MAAM,EAAE,CAAC;AACvE,WAAQ,QAAQ;AACd,UAAM,YAAY;AAClB,WAAO,MAAM,KAAK,IAAI;;UAElB;AACN;;;AAGJ,KAAI;EACF,MAAM,QAAQ,IAAI,OAAO,QAAQ;AACjC,UAAQ,QAAQ;AACd,SAAM,YAAY;AAClB,UAAO,QAAQ,WAAW,MAAM,KAAK,IAAI;;SAErC;AACN,UAAQ,QAAQ,QAAQ;;;AAI5B,SAAS,aAAa,KAAa,MAA4B;AAC7D,QAAO;EAAE;EAAK,OAAO,EAAE,aAAa,MAAM;EAAE;;;;ACpC9C,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;AAqBzB,SAAgB,WAAW,eAAoD,KAAA,GAAmB;CAChG,MAAM,WAAW,gBAAgB,EAAE;CAEnC,MAAM,SAAS,WAAW,SAAS,OAAO,IAAI;CAC9C,MAAM,UAAU,WAAW,SAAS,QAAQ,IAAI;CAChD,MAAM,UAAU,WAAW,SAAS,QAAQ,IAAI;CAEhD,MAAM,QAAQ,SAAS,SAAS,MAAM,IAAI;CAC1C,MAAM,0BAA0B,SAAS,SAAS,wBAAwB,IAAI;CAE9E,MAAM,qBAAqB,SAAS,SAAS,QAAQ,KAAK;CAC1D,MAAM,WAAW,WAAW,MAAM,YAAY;AAE9C,QAAO;EACL;EACA;EACA;EACA;EACA;EACA,QAAQ,kBAAkB,SAAS,OAAO;EAC1C,SAAS,YAAY,CAAC;EACvB;;AAGH,SAAS,WAAW,OAAoC;AACtD,QAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA;;AAGjE,SAAS,SAAS,OAAqC;AACrD,QAAO,OAAO,UAAU,YAAY,QAAQ,KAAA;;;;ACpE9C,MAAM,SAAS;AAOf,SAAgB,aAAa,cAA+B;AAC1D,QAAO;EACL,OAAO,gBAAgB,QAAQ,QAAQ,OAAO,MAAM,GAAG,OAAO,GAAG,IAAI,IAAI,SAAS;EAClF,OAAO,QAAQ,QAAQ,OAAO,MAAM,GAAG,OAAO,GAAG,IAAI,IAAI;EAC1D;;;;ACNH,MAAM,aAAa;AAwBnB,MAAM,gBAAA;;AAmBN,SAAgB,iBAAiB,QAAqB,SAA0C;CAC9F,MAAM,QAAQ,OAAO,MAAM,KAAK,SAAS,WAAW,MAAM,QAAQ,CAAC;AAKnE,QAAO,EAAE,eAAe,CAJM;EAC5B,UAAU,EAAE,YAAY,eAAe,EAAE;EACzC,YAAY,CAAC;GAAE,OAAO;IAAE,MAAM;IAAY,SAAS;IAAe;GAAE;GAAO,CAAC;EAC7E,CAC2B,EAAE;;AAKhC,SAAS,WAAW,MAAkB,SAAiC;CACrE,MAAM,UAAU,OAAO,KAAK,QAAQ;CACpC,MAAM,QAAQ,OAAO,KAAK,SAAS,KAAK,QAAQ;CAEhD,MAAM,QAAwB,EAAE;AAChC,MAAK,MAAM,CAAC,QAAQ,UAAU,OAAO,QAAQ,KAAK,MAAM,EAAE;AACxD,MAAI,UAAU,KAAA,KAAa,UAAU,KAAM;EAC3C,MAAM,UAAU,OAAO,SAAS,SAAS;AACzC,MAAI,WAAW,CAAC,QAAQ,wBAAyB;EAEjD,MAAM,KAAK,WADC,UAAU,OAAO,MAAM,GAAG,GAAiB,GAAG,QAC/B,MAAM;AACjC,MAAI,OAAO,KAAA,EAAW,OAAM,KAAK,GAAG;;AAKtC,OAAM,KAAK,KAAK,6BAA6B,QAAQ,wBAAwB,CAAC;AAE9E,KAAI,KAAK,UAAU,KAAA,EACjB,OAAM,KAAK,IAAI,iCAAiC,KAAK,IAAI,GAAG,KAAK,QAAQ,KAAK,QAAQ,CAAC,CAAC;CAE1F,MAAM,gBAAgB,iBAAiB,OAAO,QAAQ,OAAO;CAE7D,MAAM,aAAa,KAAK,YAAY,UAAU,IAAI;AAClD,QAAO;EACL,SAAS,KAAK;EACd,QAAQ,KAAK;EACb,cAAc,KAAK;EACnB,MAAM,KAAK;EAIX,MAAM;EACN,mBAAmB;EACnB,iBAAiB;EACjB,YAAY;EACZ,QAAQ,EAAE,MAAM,YAAY;EAC7B;;AAGH,SAAS,WAAW,KAAa,OAA4C;AAC3E,KAAI,UAAU,KAAA,KAAa,UAAU,KAAM,QAAO,KAAA;AAClD,KAAI,OAAO,UAAU,SAAU,QAAO,IAAI,KAAK,MAAM;AACrD,KAAI,OAAO,UAAU,UAAW,QAAO,KAAK,KAAK,MAAM;AACvD,KAAI,OAAO,UAAU,SACnB,QAAO,OAAO,UAAU,MAAM,GAAG,IAAI,KAAK,MAAM,GAAG;EAAE;EAAK,OAAO,EAAE,aAAa,OAAO;EAAE;AAK3F,QAAO,IAAI,KAAKA,WAAS,MAAM,CAAC;;AAKlC,SAAS,gBAAgC;AACvC,QAAO;EACL,IAAI,gBAAgB,WAAW;EAC/B,IAAI,mBAAmB,cAAc;EACrC,IAAI,aAAa,UAAU,CAAC;EAC5B,IAAI,aAAa,MAAM,CAAC;EACxB,IAAI,WAAW,UAAU,CAAC;EAC1B,IAAI,cAAc,SAAS,CAAC;EAC7B;;AAGH,SAAS,IAAI,KAAa,OAA6B;AACrD,QAAO;EAAE;EAAK,OAAO,EAAE,aAAa,OAAO;EAAE;;AAG/C,SAAS,IAAI,KAAa,OAA6B;AACrD,QAAO;EAAE;EAAK,OAAO,EAAE,UAAU,OAAO,KAAK,MAAM,MAAM,CAAC,EAAE;EAAE;;AAGhE,SAAS,KAAK,KAAa,OAA8B;AACvD,QAAO;EAAE;EAAK,OAAO,EAAE,WAAW,OAAO;EAAE;;AAG7C,SAAS,OAAO,IAAoB;AAClC,SAAQ,OAAO,KAAK,MAAM,GAAG,CAAC,GAAG,UAAY,UAAU;;AAGzD,SAASA,WAAS,OAAwB;AACxC,KAAI;AACF,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,SAAO,KAAK,UAAU,MAAM;SACtB;AACN,SAAO;;;;;ACnGX,MAAM,gBAA0C,IAAI,IAAI;CAAC;CAAU;CAAQ;CAAa;CAAO,CAAC;;;;;AAMhG,SAAgB,iBAAiB,KAAmC;AAClE,KAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,KAAA;CAC5C,MAAM,MAAM;CACZ,MAAM,OAAO,WAAW,IAAI,KAAK;AAGjC,KAAI,MAAM,QAAQ,IAAI,MAAM,EAAE;EAC5B,MAAM,QAAuB,EAAE;AAC/B,OAAK,MAAM,KAAK,IAAI,MAClB,KAAI,KAAK,OAAO,MAAM,SAAU,OAAM,KAAK,EAAiB;AAE9D,SAAO;GAAE;GAAM,OAAO,MAAM,SAAS,IAAI,QAAQ,CAAC;IAAE,MAAM;IAAQ,SAAS,SAAS,IAAI;IAAE,CAAC;GAAE;;CAG/F,MAAM,UAAU,IAAI,WAAW,IAAI,QAAQ,IAAI;AAK/C,KAAI,SAAS,UAAU,IAAI,iBAAiB,KAAA,EAC1C,QAAO;EACL;EACA,OAAO,CACL;GACE,MAAM;GACN,IAAI,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;GAC9D,UAAU,WAAW,SAAS,IAAI;GACnC,CACF;EACF;AAGH,KAAI,OAAO,YAAY,UAAU;EAC/B,MAAM,QAAuB,CAAC;GAAE,MAAM;GAAQ;GAAS,CAAC;AAExD,kBAAgB,OAAO,IAAI,WAAW;AACtC,SAAO;GAAE;GAAM;GAAO;;AAGxB,KAAI,MAAM,QAAQ,QAAQ,EAAE;EAC1B,MAAM,QAAuB,EAAE;AAC/B,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,OAAO,eAAe,MAAM;AAClC,OAAI,KAAM,OAAM,KAAK,KAAK;;AAE5B,kBAAgB,OAAO,IAAI,WAAW;AACtC,MAAI,MAAM,WAAW,EAAG,OAAM,KAAK;GAAE,MAAM;GAAQ,SAAS,SAAS,QAAQ;GAAE,CAAC;AAChF,SAAO;GAAE;GAAM;GAAO;;AAIxB,QAAO;EAAE;EAAM,OAAO,CAAC;GAAE,MAAM;GAAQ,SAAS,SAAS,IAAI;GAAE,CAAC;EAAE;;;AAIpE,SAAgB,kBAAkB,KAA2B;CAC3D,MAAM,MAAiB,EAAE;AACzB,MAAK,MAAM,KAAK,KAAK;EACnB,MAAM,OAAO,iBAAiB,EAAE;AAChC,MAAI,KAAM,KAAI,KAAK,KAAK;;AAE1B,QAAO;;;AAIT,SAAgB,sBAAsB,QAAyB;AAC7D,QAAO;EAAE,MAAM;EAAQ,OAAO,CAAC;GAAE,MAAM;GAAQ,SAAS;GAAQ,CAAC;EAAE;;;AAIrE,SAAgB,2BAA2B,gBAA0B,eAAiC;AACpG,KAAI,kBAAkB,KAAA,GAAW;EAC/B,MAAM,OAAO,iBAAiB,cAAc;AAC5C,MAAI,KAAM,QAAO;GAAE,GAAG;GAAM,MAAM;GAAa;;CAEjD,MAAM,QAAuB,EAAE;AAC/B,MAAK,MAAM,QAAQ,eACjB,KAAI,KAAK,SAAS,EAAG,OAAM,KAAK;EAAE,MAAM;EAAQ,SAAS;EAAM,CAAC;AAElE,KAAI,MAAM,WAAW,EAAG,OAAM,KAAK;EAAE,MAAM;EAAQ,SAAS;EAAI,CAAC;AACjE,QAAO;EAAE,MAAM;EAAa;EAAO;;;;;;;AAQrC,SAAgB,wBAAwB,QAA+B;AACrE,QAAO,CAAC;EAAE,MAAM;EAAQ,SAAS;EAAQ,CAAC;;AAK5C,SAAS,WAAW,KAA2B;AAC7C,KAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAO,cAAc,IAAI,IAAmB,GAAI,MAAsB;;AAGxE,SAAS,eAAe,KAAuC;AAC7D,KAAI,OAAO,QAAQ,SAAU,QAAO;EAAE,MAAM;EAAQ,SAAS;EAAK;AAClE,KAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,KAAA;CAC5C,MAAM,MAAM;AAGZ,KAAI,OAAO,IAAI,SAAS,aAAa,OAAO,IAAI,YAAY,YAAY,IAAI,YAAY,KAAA;MAGlF,IAAI,SAAS,UAAU,OAAO,IAAI,YAAY,SAChD,QAAO;GAAE,MAAM;GAAQ,SAAS,IAAI;GAAS;;CAIjD,MAAM,OAAO,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAEvD,KAAI,SAAS,UAAU,OAAO,IAAI,SAAS,SACzC,QAAO;EAAE,MAAM;EAAQ,SAAS,IAAI;EAAM;AAE5C,KAAI,SAAS,WACX,QAAO;EACL,MAAM;EACN,IAAI,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK;EAC1C,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;EAChD,WAAW,IAAI,SAAS,EAAE;EAC3B;AAEH,KAAI,SAAS,YAEX,QAAO;EACL,MAAM;EACN,IAAI,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK;EAC1C,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;EAChD,WAAW,IAAI,aAAa,IAAI,SAAS,EAAE;EAC5C;AAEH,KAAI,SAAS,cACX,QAAO;EACL,MAAM;EACN,IAAI,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;EAC5D,UAAU,IAAI,WAAW;EAC1B;AAEH,KAAI,SAAS,qBACX,QAAO;EACL,MAAM;EACN,IAAI,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK;EAC1C,UAAU,IAAI,YAAY;EAC3B;AAEH,KAAI,SAAS,cAAc,OAAO,IAAI,aAAa,SACjD,QAAO;EAAE,MAAM;EAAa,SAAS,IAAI;EAAU;AAErD,KAAI,SAAS,eAAe,OAAO,IAAI,YAAY,SACjD,QAAO;EAAE,MAAM;EAAa,SAAS,IAAI;EAAS;AAEpD,KAAI,SAAS,WAAW,IAAI,UAAU,OAAO,IAAI,WAAW,UAAU;EACpE,MAAM,MAAM,IAAI;EAChB,MAAM,MAAM,IAAI,QAAQ,IAAI,OAAO,QAAQ,IAAI,cAAc,gBAAgB,UAAU,IAAI,SAAS;AACpG,MAAI,IAAK,QAAO;GAAE,MAAM;GAAO,UAAU;GAAS;GAAK;;AAIzD,QAAO;EAAE;EAAM,SAAS,SAAS,IAAI;EAAE;;;;;;;AAQzC,SAAS,gBAAgB,OAAsB,KAAoB;AACjE,KAAI,CAAC,MAAM,QAAQ,IAAI,CAAE;AACzB,MAAK,MAAM,MAAM,KAAK;AACpB,MAAI,CAAC,MAAM,OAAO,OAAO,SAAU;EACnC,MAAM,IAAI;EACV,MAAM,KAAK,EAAE;EACb,IAAI,aAAsB,IAAI;AAC9B,MAAI,OAAO,eAAe,SACxB,KAAI;AACF,gBAAa,KAAK,MAAM,WAAW;UAC7B;AAIV,QAAM,KAAK;GACT,MAAM;GACN,IAAI,OAAO,EAAE,OAAO,WAAW,EAAE,KAAK;GACtC,MAAM,IAAI,QAAQ;GAClB,WAAW,cAAc,EAAE;GAC5B,CAAC;;;AAIN,SAAS,SAAS,OAAwB;AACxC,KAAI;AACF,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,SAAO,KAAK,UAAU,MAAM;SACtB;AACN,SAAO;;;;;AClIX,MAAM,uBAAuB,OAAU;AACvC,MAAM,oBAAoB;AAW1B,IAAa,cAAb,MAAyB;CACvB,uBAAwB,IAAI,KAAuB;CACnD,gCAAiC,IAAI,KAA2B;CAEhE,gBAAwB;AACtB,SAAO,KAAK,KAAK;;;;;;;;CASnB,mBAAmB,KAAoC,KAAiC;EACtF,MAAM,QAAQ,IAAI;AAClB,MAAI,CAAC,MAAO;AACZ,MAAI,KAAK,KAAK,IAAI,MAAM,CAAE;EAE1B,MAAM,OAAO,KAAK,cAAc,IAAI,MAAM;EAC1C,MAAM,UAAU,MAAM,WAAW,QAAQ,OAAO,GAAG;EACnD,MAAM,eAAe,MAAM,kBAAkB;EAE7C,MAAM,QAAoB;GACxB,QAAQ,QAAQ,GAAG,QAAQ,GAAG,MAAM,SAAS,GAAG;GAChD;GACA;GACA,MAAM;GACN,SAAS,KAAK,KAAK;GACnB,OAAO,KAAA;GACP,OAAO;IACL,GAAG,WAAW,IAAI;IAClB,GAAG,cAAc,IAAI;IACrB,GAAG,aAAa,IAAI;IACpB,mBAAmB;IAKnB,mCAAmC,IAAI;IACvC,qCAAqC,IAAI,WAAW,kBAAkB,IAAI,SAAS,GAAG,KAAA;IACvF;GACF;AAED,OAAK,KAAK,IAAI,OAAO;GACnB;GACA,SAAS,EAAE;GACX,gCAAgB,IAAI,KAAK;GACzB,+BAAe,IAAI,KAAK;GACxB,gBAAgB,KAAA;GAChB,QAAQ,EAAE;GACV,oCAAoB,IAAI,KAAK;GAC9B,CAAC;;;;;;;;;;;CAYJ,WAAW,KAA4B,KAAiC;EACtE,MAAM,MAAM,KAAK,KAAK,IAAI,IAAI,SAAS,IAAI,MAAM;AACjD,MAAI,CAAC,IAAK;EAGV,MAAM,gBAA2B,CAAC,GADR,kBAAkB,IAAI,gBAAgB,CACT;AACvD,MAAI,IAAI,OAAQ,eAAc,KAAK,sBAAsB,IAAI,OAAO,CAAC;AAErE,SAAO,OAAO,IAAI,MAAM,OAAO;GAC7B,oCAAoC,IAAI,eAAe,wBAAwB,IAAI,aAAa,GAAG,KAAA;GACnG,qBAAqB,IAAI;GACzB,+BAA+B;GAC/B,yBAAyB,IAAI;GAC7B,wBAAwB,IAAI;GAC5B,iBAAiB,IAAI;GACrB,qBAAqB,IAAI;GAC1B,CAAC;AAMF,MAAI,UAAU;;;;;;CAOhB,YAAY,KAA6B,KAAiC;EACxE,MAAM,MAAM,KAAK,KAAK,IAAI,IAAI,SAAS,IAAI,MAAM;AACjD,MAAI,CAAC,IAAK;EACV,MAAM,mBAAmB,2BAA2B,IAAI,gBAAgB,IAAI,cAAc;AAC1F,SAAO,OAAO,IAAI,MAAM,OAAO;GAC7B,gCAAgC,CAAC,iBAAiB;GAClD,yBAAyB,IAAI;GAC7B,uBAAuB,IAAI;GAC3B,yBAAyB,IAAI;GAC7B,GAAG,WAAW,IAAI,MAAM;GACzB,CAAC;;CAGJ,mBAAmB,KAAoC,KAAiC;EACtF,MAAM,MAAM,KAAK,KAAK,IAAI,IAAI,MAAM;AACpC,MAAI,CAAC,IAAK;EACV,MAAM,OAAmB;GACvB,QAAQ,QAAQ,GAAG,IAAI,MAAM,QAAQ,cAAc,IAAI,UAAU,GAAG;GACpE,SAAS,IAAI,MAAM;GACnB,cAAc,IAAI,MAAM;GACxB,MAAM;GACN,SAAS,KAAK,KAAK;GACnB,OAAO,KAAA;GACP,OAAO;IACL,GAAG,cAAc,IAAI;IACrB,GAAG,aAAa,IAAI;IACpB,mBAAmB,IAAI;IACvB,oBAAoB,IAAI;IACxB,iBAAiB,IAAI;IACrB,qBAAqB,IAAI;IACzB,wBAAwB,IAAI;IAC5B,gBAAgB,IAAI;IACpB,sBAAsB,IAAI;IAG1B,+BAA+B,CAAC,GAAG,IAAI,QAAQ;IAChD;GACF;AACD,MAAI,eAAe,IAAI,IAAI,QAAQ,KAAK;;CAG1C,iBAAiB,KAAkC,MAAkC;EACnF,MAAM,MAAM,KAAK,KAAK,IAAI,IAAI,MAAM;AACpC,MAAI,CAAC,IAAK;EACV,MAAM,OAAO,IAAI,eAAe,IAAI,IAAI,OAAO;AAC/C,MAAI,CAAC,KAAM;AACX,OAAK,QAAQ,KAAK,KAAK;AACvB,OAAK,UAAU,IAAI,YAAY,cAAc,OAAO;AACpD,OAAK,eAAe,IAAI;AACxB,SAAO,OAAO,KAAK,OAAO;GACxB,wBAAwB,IAAI;GAC5B,oBAAoB,IAAI;GACxB,2BAA2B,IAAI;GAC/B,yBAAyB,IAAI;GAC7B,kCAAkC,IAAI;GACtC,kCAAkC,IAAI;GACtC,oBAAoB,IAAI;GACxB,qCAAqC,IAAI;GAC1C,CAAC;AACF,MAAI,eAAe,OAAO,IAAI,OAAO;AACrC,MAAI,OAAO,KAAK,KAAK;;;;;;;;;;;;CAavB,iBAAiB,KAAkC,KAAiC;AAClF,MAAI,CAAC,IAAI,MAAO;EAChB,MAAM,MAAM,KAAK,KAAK,IAAI,IAAI,MAAM;AACpC,MAAI,CAAC,IAAK;EAEV,MAAM,aAAa,IAAI,cAAc,GAAG,IAAI,SAAS,GAAG,YAAY;EACpE,MAAM,OAAmB;GACvB,QAAQ,QAAQ,GAAG,IAAI,MAAM,QAAQ,aAAa,cAAc,GAAG;GACnE,SAAS,IAAI,MAAM;GACnB,cAAc,IAAI,MAAM;GACxB,MAAM,aAAa,IAAI;GACvB,SAAS,KAAK,KAAK;GACnB,OAAO,KAAA;GACP,OAAO;IACL,GAAG,cAAc,IAAI;IACrB,GAAG,aAAa,IAAI;IACpB,mBAAmB,IAAI;IACvB,oBAAoB,IAAI;IACxB,uBAAuB;IACvB,oCAAoC,IAAI;IACzC;GACF;AACD,MAAI,cAAc,IAAI,YAAY,KAAK;AAIvC,MAAI,QAAQ,KAAK;GACf,MAAM;GACN,OAAO,CAAC;IAAE,MAAM;IAAa,IAAI;IAAY,MAAM,IAAI;IAAU,WAAW,IAAI;IAAQ,CAAC;GAC1F,CAAC;;CAGJ,gBAAgB,KAAiC,MAAkC;AACjF,MAAI,CAAC,IAAI,MAAO;EAChB,MAAM,MAAM,KAAK,KAAK,IAAI,IAAI,MAAM;AACpC,MAAI,CAAC,IAAK;EAUV,IAAI,aACF,IAAI,cAAc,IAAI,cAAc,IAAI,IAAI,WAAW,GAAG,IAAI,aAAa,KAAA;AAC7E,MAAI,CAAC,WAAY,cAAa,KAAK,uBAAuB,KAAK,IAAI,SAAS;AAC5E,MAAI,CAAC,WAAY;EACjB,MAAM,OAAO,IAAI,cAAc,IAAI,WAAW;AAC9C,MAAI,CAAC,KAAM;EACX,MAAM,aAAqB;AAE3B,OAAK,QAAQ,KAAK,KAAK;AAEvB,OAAK,UADW,QAAQ,IAAI,MAAM,GACT,UAAU;AACnC,OAAK,eAAe,IAAI;AACxB,SAAO,OAAO,KAAK,OAAO;GACxB,iCAAiC,IAAI;GACrC,gCAAgC,IAAI;GACpC,wBAAwB,IAAI;GAC7B,CAAC;AACF,MAAI,cAAc,OAAO,WAAW;AACpC,MAAI,OAAO,KAAK,KAAK;AAIrB,MAAI,QAAQ,KAAK;GACf,MAAM;GACN,OAAO,CAAC;IAAE,MAAM;IAAsB,IAAI;IAAY,UAAU,IAAI,UAAU,IAAI,SAAS;IAAI,CAAC;GACjG,CAAC;;CAGJ,mBAAmB,KAAoC,KAAiC;EACtF,MAAM,QAAQ,IAAI;AAClB,MAAI,CAAC,MAAO;EACZ,MAAM,MAAM,KAAK,KAAK,IAAI,MAAM;AAChC,MAAI,CAAC,IAAK;AAiBV,MAAI,iBAhBqB;GACvB,QAAQ,QAAQ,GAAG,IAAI,MAAM,QAAQ,cAAc,IAAI,OAAO,UAAU,GAAG;GAC3E,SAAS,IAAI,MAAM;GACnB,cAAc,IAAI,MAAM;GACxB,MAAM;GACN,SAAS,KAAK,KAAK;GACnB,OAAO,KAAA;GACP,OAAO;IACL,GAAG,cAAc,IAAI;IACrB,GAAG,aAAa,IAAI;IACpB,mBAAmB;IACnB,4CAA4C,IAAI;IAChD,oCAAoC,IAAI;IACxC,oCAAoC,IAAI,WAAW,kBAAkB,IAAI,SAAS,GAAG,KAAA;IACtF;GACF;;CAIH,kBAAkB,KAAmC,KAAiC;EACpF,MAAM,QAAQ,IAAI;AAClB,MAAI,CAAC,MAAO;EACZ,MAAM,MAAM,KAAK,KAAK,IAAI,MAAM;AAChC,MAAI,CAAC,KAAK,eAAgB;EAC1B,MAAM,OAAO,IAAI;AACjB,OAAK,QAAQ,KAAK,KAAK;AACvB,OAAK,UAAU;AACf,SAAO,OAAO,KAAK,OAAO;GACxB,2CAA2C,IAAI;GAC/C,uCAAuC,IAAI;GAC3C,mCAAmC,IAAI;GACxC,CAAC;AACF,MAAI,iBAAiB,KAAA;AACrB,MAAI,OAAO,KAAK,KAAK;;;;;;CAOvB,kBAAkB,KAAmC,KAAiC;EACpF,MAAM,cAAc,IAAI;AACxB,MAAI,CAAC,YAAa;EAClB,MAAM,SAAS,KAAK,KAAK,IAAI,YAAY;AACzC,MAAI,CAAC,OAAQ;EAEb,MAAM,OAAmB;GACvB,QAAQ,QAAQ,GAAG,OAAO,MAAM,QAAQ,YAAY,IAAI,SAAS,GAAG;GACpE,SAAS,OAAO,MAAM;GACtB,cAAc,OAAO,MAAM;GAC3B,MAAM;GACN,SAAS,KAAK,KAAK;GACnB,OAAO,KAAA;GACP,OAAO;IAML,GAAG,cAAc,IAAI;IACrB,GAAG,aAAa,IAAI;IACpB,0BAA0B;IAC1B,mBAAmB,IAAI;IACvB,uCAAuC,IAAI;IAC3C,8BAA8B,IAAI;IAClC,2BAA2B,IAAI;IAC/B,0BAA0B,IAAI;IAC9B,sCAAsC,IAAI;IAC1C,uCAAuC,IAAI,WAAW;IACtD,0CAA0C,IAAI,WAAW;IACzD,kCAAkC,IAAI,WAAW;IACjD,yCACE,OAAO,IAAI,WAAW,aAAa,YAAY,OAAO,IAAI,WAAW,aAAa,WAC9E,OAAO,IAAI,UAAU,SAAS,GAC9B,KAAA;IACP;GACF;AACD,SAAO,mBAAmB,IAAI,IAAI,OAAO,KAAK;AAC9C,OAAK,yBAAyB;AAC9B,OAAK,cAAc,IAAI,IAAI,OAAO;GAChC,SAAS,OAAO,MAAM;GACtB,gBAAgB,KAAK;GACrB,WAAW,KAAK,KAAK;GACtB,CAAC;;CAGJ,gBAAgB,KAAiC,KAAiC;EAChF,MAAM,cAAc,IAAI;AACxB,MAAI,CAAC,YAAa;EAClB,MAAM,SAAS,KAAK,KAAK,IAAI,YAAY;AACzC,MAAI,CAAC,OAAQ;EACb,MAAM,aAAa,IAAI;AACvB,MAAI,CAAC,WAAY;EACjB,MAAM,OAAO,OAAO,mBAAmB,IAAI,WAAW;AACtD,MAAI,CAAC,KAAM;AAEX,OAAK,QAAQ,KAAK,KAAK;AAEvB,OAAK,UADW,IAAI,YAAY,WAAW,QAAQ,IAAI,MAAM,GACpC,UAAU;AACnC,OAAK,eAAe,IAAI;AACxB,SAAO,OAAO,KAAK,OAAO;GACxB,wCAAwC,IAAI;GAC5C,iCAAiC,IAAI;GACrC,4BAA4B,IAAI;GAChC,6BAA6B,IAAI;GACjC,mCAAmC,IAAI;GACvC,gCAAgC,IAAI;GACrC,CAAC;AACF,SAAO,mBAAmB,OAAO,WAAW;AAC5C,SAAO,OAAO,KAAK,KAAK;;;;;;;CAU1B,WAAW,KAA4B,KAAoD;EACzF,MAAM,QAAQ,IAAI;AAClB,MAAI,CAAC,MAAO,QAAO,KAAA;EACnB,MAAM,MAAM,KAAK,KAAK,IAAI,MAAM;AAChC,MAAI,CAAC,KAAK;AAGR,QAAK,cAAc,OAAO,MAAM;AAChC;;EAGF,MAAM,MAAM,KAAK,KAAK;AACtB,MAAI,MAAM,QAAQ;AAClB,MAAI,MAAM,UAAU,IAAI,UAAU,OAAO;AACzC,MAAI,MAAM,eAAe,IAAI;AAC7B,SAAO,OAAO,IAAI,MAAM,OAAO;GAC7B,wBAAwB,IAAI;GAC5B,wBAAwB,IAAI;GAC5B,gCAAgC,IAAI;GACpC,4BAA4B,kBAAkB,IAAI,SAAS;GAC5D,CAAC;AAKF,OAAK,MAAM,QAAQ,IAAI,eAAe,QAAQ,EAAE;AAC9C,QAAK,QAAQ;AACb,QAAK,UAAU;AACf,QAAK,MAAM,sBAAsB;AACjC,OAAI,OAAO,KAAK,KAAK;;AAEvB,OAAK,MAAM,QAAQ,IAAI,cAAc,QAAQ,EAAE;AAC7C,QAAK,QAAQ;AACb,QAAK,UAAU;AACf,QAAK,MAAM,sBAAsB;AACjC,OAAI,OAAO,KAAK,KAAK;;AAEvB,MAAI,IAAI,gBAAgB;AACtB,OAAI,eAAe,QAAQ;AAC3B,OAAI,eAAe,UAAU;AAC7B,OAAI,eAAe,MAAM,sBAAsB;AAC/C,OAAI,OAAO,KAAK,IAAI,eAAe;;AAErC,OAAK,MAAM,QAAQ,IAAI,mBAAmB,QAAQ,EAAE;AAClD,QAAK,QAAQ;AACb,QAAK,UAAU;AACf,QAAK,MAAM,+BAA+B;AAC1C,OAAI,OAAO,KAAK,KAAK;;EAGvB,MAAM,QAAQ,CAAC,IAAI,OAAO,GAAG,IAAI,OAAO;AACxC,OAAK,KAAK,OAAO,MAAM;AACvB,OAAK,cAAc,OAAO,MAAM;AAChC,SAAO;GAAE;GAAO;GAAO;;;CAIzB,QAAQ,OAAqB;AAC3B,OAAK,KAAK,OAAO,MAAM;AACvB,OAAK,cAAc,OAAO,MAAM;;;CAIlC,oBAA4B;AAC1B,SAAO,KAAK,cAAc;;;;;;;;;;;CAY5B,0BAAwC;EACtC,MAAM,MAAM,KAAK,KAAK;AACtB,OAAK,MAAM,CAAC,OAAO,SAAS,KAAK,cAC/B,KAAI,MAAM,KAAK,YAAY,qBACzB,MAAK,cAAc,OAAO,MAAM;AAGpC,MAAI,KAAK,cAAc,QAAQ,kBAAmB;EAClD,MAAM,SAAS,MAAM,KAAK,KAAK,cAAc,SAAS,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,YAAY,EAAE,GAAG,UAAU;EACvG,MAAM,WAAW,KAAK,cAAc,OAAO;AAC3C,OAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAAK;GACjC,MAAM,QAAQ,OAAO;AACrB,OAAI,MAAO,MAAK,cAAc,OAAO,MAAM,GAAG;;;CAIlD,uBAA+B,KAAe,UAAsC;EAMlF,MAAM,SAAS,aAAa;EAC5B,MAAM,UAAU,MAAM,KAAK,IAAI,cAAc,SAAS,CAAC;AACvD,OAAK,IAAI,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;GAC5C,MAAM,QAAQ,QAAQ;AACtB,OAAI,CAAC,MAAO;GACZ,MAAM,CAAC,IAAI,QAAQ;AACnB,OAAI,KAAK,SAAS,OAAQ,QAAO;;;;AAQvC,SAAS,WAAW,KAAsC;AACxD,QAAO;EACL,mBAAmB,IAAI;EACvB,uBAAuB,IAAI;EAC3B,wBAAwB,IAAI;EAC5B,qBAAqB,IAAI;EACzB,uBAAuB,IAAI;EAC3B,0BAA0B,IAAI;EAC9B,6BAA6B,IAAI;EACjC,oBAAoB,IAAI;EACxB,uBAAuB,IAAI;EAC3B,wBAAwB,IAAI;EAC5B,8BAA8B,IAAI;EAClC,qBAAqB,IAAI;EAC1B;;;;;;;;;;AAWH,SAAS,aAAa,KAAsC;AAC1D,KAAI,CAAC,IAAI,UAAW,QAAO,EAAE;AAC7B,QAAO;EACL,cAAc,IAAI;EAClB,qBAAqB,IAAI;EAC1B;;;;;;;;;;;;;;;;;;;AAoBH,SAAS,cAAc,KAAsC;CAC3D,MAAM,OAAiB,EAAE;AACzB,KAAI,IAAI,QAAS,MAAK,KAAK,IAAI,QAAQ;AACvC,KAAI,IAAI,UAAW,MAAK,KAAK,IAAI,UAAU;AAC3C,KAAI,IAAI,QACN,MAAK,KAAK,IAAI,YAAY,UAAU,IAAI,QAAQ,QAAQ,IAAI,UAAU,IAAI,QAAQ;CAGpF,MAAM,WAAmC,EAAE;AAC3C,KAAI,IAAI,MAAO,UAAS,qBAAqB,IAAI;AACjD,KAAI,IAAI,UAAW,UAAS,yBAAyB,IAAI;AACzD,KAAI,IAAI,WAAY,UAAS,0BAA0B,IAAI;AAC3D,KAAI,IAAI,QAAS,UAAS,uBAAuB,IAAI;AACrD,KAAI,IAAI,aAAc,UAAS,4BAA4B,IAAI;AAC/D,KAAI,IAAI,UAAW,UAAS,yBAAyB,IAAI;AACzD,KAAI,IAAI,gBAAiB,UAAS,+BAA+B,IAAI;AACrE,KAAI,IAAI,QAAS,UAAS,sBAAsB,IAAI;AACpD,KAAI,IAAI,MAAO,UAAS,0BAA0B,IAAI;AACtD,KAAI,IAAI,gBAAiB,UAAS,gCAAgC,IAAI;AACtE,KAAI,IAAI,QAAS,UAAS,uBAAuB,IAAI;AAErD,QAAO;EACL,iBAAiB,KAAK,SAAS,IAAI,OAAO,KAAA;EAC1C,qBAAqB,OAAO,KAAK,SAAS,CAAC,SAAS,IAAI,WAAW,KAAA;EACpE;;AAGH,SAAS,WAAW,OAAgD;AAClE,KAAI,CAAC,MAAO,QAAO,EAAE;AACrB,QAAO;EACL,6BAA6B,MAAM;EACnC,8BAA8B,MAAM;EACpC,wCAAwC,MAAM;EAC9C,4CAA4C,MAAM;EAClD,6BAA6B,MAAM;EACpC;;AAGH,SAAS,QAAQ,OAAe,QAAwB;AACtD,QAAO,WAAW,SAAS,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,OAAO;;;;;;;;;;;;;;;;ACvnB1E,SAAwB,uBAAuB,KAA4B,OAAwB,EAAE,EAAQ;CAI3G,MAAM,SAAS,KAAK,UAAU,WAAW,IAAI,aAAa;CAC1D,MAAM,SAAS,KAAK,UAAU,aAAa,OAAO,MAAM;AAExD,KAAI,CAAC,OAAO,SAAS;AACnB,MAAI,OAAO,WAAW,GAAI,QAAO,MAAM,oEAAoE;AAC3G,MAAI,OAAO,YAAY,GAAI,QAAO,MAAM,sEAAsE;AAC9G;;AAEF,QAAO,MACL,oBAAoB,OAAO,QAAQ,QAAQ,OAAO,QAAQ,2BAA2B,OAAO,0BAC7F;CAED,MAAM,UAAU,IAAI,aAAa;CAGjC,MAAM,QACJ,MACA,OAC2C;AAC3C,UAAQ,KAAK,QAAQ;AACnB,OAAI;AACF,OAAG,KAAU,IAA4B;YAClC,KAAK;AACZ,WAAO,KAAK,GAAG,KAAK,mBAAmB,OAAO,IAAI,GAAG;;;;AAO3D,KAAI,GACF,sBACA,KAAoC,uBAAuB,KAAK,QAAQ;AACtE,UAAQ,mBAAmB,KAAK,IAAI;GACpC,CACH;AAED,KAAI,GACF,sBACA,KAAoC,uBAAuB,KAAK,QAAQ;AACtE,UAAQ,mBAAmB,KAAK,IAAI;GACpC,CACH;AACD,KAAI,GACF,oBACA,KAAkC,qBAAqB,KAAK,QAAQ;AAClE,UAAQ,iBAAiB,KAAK,IAAI;GAClC,CACH;AAKD,KAAI,GACF,oBACA,KAAkC,qBAAqB,KAAK,QAAQ;AAClE,UAAQ,iBAAiB,KAAK,IAAI;GAClC,CACH;AACD,KAAI,GACF,mBACA,KAAiC,oBAAoB,KAAK,QAAQ;AAChE,UAAQ,gBAAgB,KAAK,IAAI;GACjC,CACH;AAED,KAAI,GACF,qBACA,KAAoC,sBAAsB,KAAK,QAAQ;AACrE,UAAQ,mBAAmB,KAAK,IAAI;GACpC,CACH;AACD,KAAI,GACF,oBACA,KAAmC,qBAAqB,KAAK,QAAQ;AACnE,UAAQ,kBAAkB,KAAK,IAAI;GACnC,CACH;AAED,KAAI,GACF,oBACA,KAAmC,qBAAqB,KAAK,QAAQ;AACnE,UAAQ,kBAAkB,KAAK,IAAI;GACnC,CACH;AACD,KAAI,GACF,kBACA,KAAiC,mBAAmB,KAAK,QAAQ;AAC/D,UAAQ,gBAAgB,KAAK,IAAI;GACjC,CACH;AAOD,KAAI,GACF,aACA,KAA4B,cAAc,KAAK,QAAQ;AACrD,UAAQ,WAAW,KAAK,IAAI;GAC5B,CACH;AACD,KAAI,GACF,cACA,KAA6B,eAAe,KAAK,QAAQ;AACvD,UAAQ,YAAY,KAAK,IAAI;GAC7B,CACH;AAmCD,KAAI,GACF,aACA,KAA4B,cAAc,KAAK,QAAQ;AACrD,uBAAqB;AACnB,OAAI;IACF,MAAM,SAAS,QAAQ,WAAW,KAAK,IAAI;AAC3C,QAAI,CAAC,QAAQ;AACX,YAAO,MAAM,mDAAmD;AAChE;;AAEF,SAAK,SAAS,OAAO;IACrB,MAAM,UAAU,iBAAiB,QAAQ;KACvC,yBAAyB,OAAO;KAChC,QAAQ,OAAO;KAChB,CAAC;AACG,eAAW;KACd,SAAS,OAAO;KAChB,QAAQ,OAAO;KACf,SAAS,OAAO;KAChB;KACA;KACD,CAAC;YACK,KAAK;AACZ,WAAO,KAAK,8BAA8B,OAAO,IAAI,GAAG;;IAE1D;GACF,CACH"}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "@latitude-data/openclaw-telemetry",
|
|
3
3
|
"name": "Latitude Telemetry",
|
|
4
4
|
"description": "Streams every OpenClaw agent run to Latitude as OTLP traces — full prompt, message history, assistant output, tool I/O, token usage, and agent name.",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.9",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latitude-data/openclaw-telemetry",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "OpenClaw plugin that streams LLM calls, tool executions, and agent runs to Latitude as OTLP traces",
|
|
5
5
|
"author": "Latitude Data SL <hello@latitude.so>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"tsdown": "0.21.9",
|
|
49
49
|
"@typescript/native-preview": "7.0.0-dev.20260421.2",
|
|
50
50
|
"typescript": "6.0.3",
|
|
51
|
-
"vitest": "4.1.
|
|
51
|
+
"vitest": "4.1.8"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "tsdown",
|