@latitude-data/openclaw-telemetry 0.0.6 → 0.0.8

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/LICENSE CHANGED
@@ -1,157 +1,21 @@
1
- # GNU LESSER GENERAL PUBLIC LICENSE
2
-
3
- Version 3, 29 June 2007
4
-
5
- Copyright (C) 2007 Free Software Foundation, Inc.
6
- <https://fsf.org/>
7
-
8
- Everyone is permitted to copy and distribute verbatim copies of this
9
- license document, but changing it is not allowed.
10
-
11
- This version of the GNU Lesser General Public License incorporates the
12
- terms and conditions of version 3 of the GNU General Public License,
13
- supplemented by the additional permissions listed below.
14
-
15
- ## 0. Additional Definitions.
16
-
17
- As used herein, "this License" refers to version 3 of the GNU Lesser
18
- General Public License, and the "GNU GPL" refers to version 3 of the
19
- GNU General Public License.
20
-
21
- "The Library" refers to a covered work governed by this License, other
22
- than an Application or a Combined Work as defined below.
23
-
24
- An "Application" is any work that makes use of an interface provided
25
- by the Library, but which is not otherwise based on the Library.
26
- Defining a subclass of a class defined by the Library is deemed a mode
27
- of using an interface provided by the Library.
28
-
29
- A "Combined Work" is a work produced by combining or linking an
30
- Application with the Library. The particular version of the Library
31
- with which the Combined Work was made is also called the "Linked
32
- Version".
33
-
34
- The "Minimal Corresponding Source" for a Combined Work means the
35
- Corresponding Source for the Combined Work, excluding any source code
36
- for portions of the Combined Work that, considered in isolation, are
37
- based on the Application, and not on the Linked Version.
38
-
39
- The "Corresponding Application Code" for a Combined Work means the
40
- object code and/or source code for the Application, including any data
41
- and utility programs needed for reproducing the Combined Work from the
42
- Application, but excluding the System Libraries of the Combined Work.
43
-
44
- ## 1. Exception to Section 3 of the GNU GPL.
45
-
46
- You may convey a covered work under sections 3 and 4 of this License
47
- without being bound by section 3 of the GNU GPL.
48
-
49
- ## 2. Conveying Modified Versions.
50
-
51
- If you modify a copy of the Library, and, in your modifications, a
52
- facility refers to a function or data to be supplied by an Application
53
- that uses the facility (other than as an argument passed when the
54
- facility is invoked), then you may convey a copy of the modified
55
- version:
56
-
57
- - a) under this License, provided that you make a good faith effort
58
- to ensure that, in the event an Application does not supply the
59
- function or data, the facility still operates, and performs
60
- whatever part of its purpose remains meaningful, or
61
- - b) under the GNU GPL, with none of the additional permissions of
62
- this License applicable to that copy.
63
-
64
- ## 3. Object Code Incorporating Material from Library Header Files.
65
-
66
- The object code form of an Application may incorporate material from a
67
- header file that is part of the Library. You may convey such object
68
- code under terms of your choice, provided that, if the incorporated
69
- material is not limited to numerical parameters, data structure
70
- layouts and accessors, or small macros, inline functions and templates
71
- (ten or fewer lines in length), you do both of the following:
72
-
73
- - a) Give prominent notice with each copy of the object code that
74
- the Library is used in it and that the Library and its use are
75
- covered by this License.
76
- - b) Accompany the object code with a copy of the GNU GPL and this
77
- license document.
78
-
79
- ## 4. Combined Works.
80
-
81
- You may convey a Combined Work under terms of your choice that, taken
82
- together, effectively do not restrict modification of the portions of
83
- the Library contained in the Combined Work and reverse engineering for
84
- debugging such modifications, if you also do each of the following:
85
-
86
- - a) Give prominent notice with each copy of the Combined Work that
87
- the Library is used in it and that the Library and its use are
88
- covered by this License.
89
- - b) Accompany the Combined Work with a copy of the GNU GPL and this
90
- license document.
91
- - c) For a Combined Work that displays copyright notices during
92
- execution, include the copyright notice for the Library among
93
- these notices, as well as a reference directing the user to the
94
- copies of the GNU GPL and this license document.
95
- - d) Do one of the following:
96
- - 0) Convey the Minimal Corresponding Source under the terms of
97
- this License, and the Corresponding Application Code in a form
98
- suitable for, and under terms that permit, the user to
99
- recombine or relink the Application with a modified version of
100
- the Linked Version to produce a modified Combined Work, in the
101
- manner specified by section 6 of the GNU GPL for conveying
102
- Corresponding Source.
103
- - 1) Use a suitable shared library mechanism for linking with
104
- the Library. A suitable mechanism is one that (a) uses at run
105
- time a copy of the Library already present on the user's
106
- computer system, and (b) will operate properly with a modified
107
- version of the Library that is interface-compatible with the
108
- Linked Version.
109
- - e) Provide Installation Information, but only if you would
110
- otherwise be required to provide such information under section 6
111
- of the GNU GPL, and only to the extent that such information is
112
- necessary to install and execute a modified version of the
113
- Combined Work produced by recombining or relinking the Application
114
- with a modified version of the Linked Version. (If you use option
115
- 4d0, the Installation Information must accompany the Minimal
116
- Corresponding Source and Corresponding Application Code. If you
117
- use option 4d1, you must provide the Installation Information in
118
- the manner specified by section 6 of the GNU GPL for conveying
119
- Corresponding Source.)
120
-
121
- ## 5. Combined Libraries.
122
-
123
- You may place library facilities that are a work based on the Library
124
- side by side in a single library together with other library
125
- facilities that are not Applications and are not covered by this
126
- License, and convey such a combined library under terms of your
127
- choice, if you do both of the following:
128
-
129
- - a) Accompany the combined library with a copy of the same work
130
- based on the Library, uncombined with any other library
131
- facilities, conveyed under the terms of this License.
132
- - b) Give prominent notice with the combined library that part of it
133
- is a work based on the Library, and explaining where to find the
134
- accompanying uncombined form of the same work.
135
-
136
- ## 6. Revised Versions of the GNU Lesser General Public License.
137
-
138
- The Free Software Foundation may publish revised and/or new versions
139
- of the GNU Lesser General Public License from time to time. Such new
140
- versions will be similar in spirit to the present version, but may
141
- differ in detail to address new problems or concerns.
142
-
143
- Each version is given a distinguishing version number. If the Library
144
- as you received it specifies that a certain numbered version of the
145
- GNU Lesser General Public License "or any later version" applies to
146
- it, you have the option of following the terms and conditions either
147
- of that published version or of any later version published by the
148
- Free Software Foundation. If the Library as you received it does not
149
- specify a version number of the GNU Lesser General Public License, you
150
- may choose any version of the GNU Lesser General Public License ever
151
- published by the Free Software Foundation.
152
-
153
- If the Library as you received it specifies that a proxy can decide
154
- whether future versions of the GNU Lesser General Public License shall
155
- apply, that proxy's public statement of acceptance of any version is
156
- permanent authorization for you to choose that version for the
157
- Library.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Latitude Data SL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,28 +1,41 @@
1
1
  # @latitude-data/openclaw-telemetry
2
2
 
3
- OpenClaw plugin that streams every agent run to [Latitude](https://latitude.so) as OTLP traces. Full fidelity exact system prompt, message history, assistant output, token usage, tool I/O, and the running agent's name on every span.
3
+ OpenClaw plugin that streams every agent run to [Latitude](https://latitude.so) as OTLP traces — full system prompt, message history, assistant output, token usage, tool I/O, and the running agent's name on every span.
4
4
 
5
- ## Install
5
+ ## Requirements
6
+
7
+ - **OpenClaw 2026.4.25 or newer** on PATH.
8
+ - A **Latitude API key** from `https://console.latitude.so/projects/<your-slug>/settings/keys` and the matching **project slug**.
6
9
 
7
- Requires OpenClaw **2026.4.25 or newer**. Get a Latitude API key + project slug from `https://console.latitude.so/settings/api-keys` first.
10
+ ## Install
8
11
 
9
- > **One-shot installer coming soon.** A separate `@latitude-data/openclaw-telemetry-cli` package is in flight that will collapse the steps below into `npx -y @latitude-data/openclaw-telemetry-cli install`. For now, the manual flow below is the supported path.
12
+ ### Recommended one-shot CLI
10
13
 
11
- ### Step 1 Install the plugin runtime
14
+ The companion CLI handles every step (install, config, validate, restart) in one command:
12
15
 
13
16
  ```bash
14
- openclaw plugins install @latitude-data/openclaw-telemetry
17
+ npx -y @latitude-data/openclaw-telemetry-cli@0.0.8 install
15
18
  ```
16
19
 
17
- OpenClaw fetches the package from npm, runs its security scan, copies files into `~/.openclaw/extensions/<id>/`, and creates a (disabled) `plugins.entries["@latitude-data/openclaw-telemetry"]` entry in `~/.openclaw/openclaw.json`. The gateway will print:
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.
21
+
22
+ ### Manual install
23
+
24
+ If you'd rather not use the CLI, do exactly what it does, in four steps:
25
+
26
+ #### 1. Install the runtime
27
+
28
+ ```bash
29
+ openclaw plugins install @latitude-data/openclaw-telemetry@0.0.8
30
+ ```
18
31
 
19
- > Plugin installed but disabled. Configure it, then run `openclaw plugins enable @latitude-data/openclaw-telemetry`.
32
+ Pin to an exact version. OpenClaw's `security audit --deep` warns about unpinned install specs, so always include the `@<version>` suffix.
20
33
 
21
- That's expected the next step does the configure-and-enable.
34
+ OpenClaw fetches from npm, runs its security scan, copies files into `~/.openclaw/extensions/<id>/`, and creates a (disabled) `plugins.entries["@latitude-data/openclaw-telemetry"]` entry in `~/.openclaw/openclaw.json`.
22
35
 
23
- ### Step 2 Configure and enable
36
+ #### 2. Configure and enable
24
37
 
25
- Run these `openclaw config set` commands (use bracket notation so the scoped package name parses correctly). Substitute your real API key and project slug in the first two:
38
+ Run these `openclaw config set` commands (use bracket notation so the scoped package name parses correctly). Substitute your real API key and project slug:
26
39
 
27
40
  ```bash
28
41
  openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.apiKey' "lat_xxx"
@@ -32,19 +45,19 @@ openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].hooks.
32
45
  openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].enabled' true
33
46
  ```
34
47
 
35
- Why two `allowConversationAccess` writes? They mean different things `hooks.*` is OpenClaw's dispatch gate (without it the plugin's hook handlers never fire), `config.*` is the plugin's payload-content gate (without it the plugin emits structural-only spans). For full content capture they must both be `true`. See [The two flags](#the-two-flags) for details.
48
+ Both `allowConversationAccess` writes are requiredsee [The two flags](#the-two-flags).
36
49
 
37
- ### Step 3 Add to `plugins.allow` (optional but loud)
50
+ #### 3. Add to `plugins.allow` (optional but recommended)
38
51
 
39
- OpenClaw warns at every gateway restart about non-bundled plugins that auto-load without provenance via `plugins.allow`. Silence the warning by adding the id:
52
+ OpenClaw warns at every gateway restart about non-bundled plugins that auto-load without provenance via `plugins.allow`. Silence the warning:
40
53
 
41
54
  ```bash
42
- # `config set` can't append to arrays — set the whole list. If you already have
43
- # entries in `plugins.allow`, include them here too:
55
+ # `config set` can't append to arrays — set the whole list. Include any other
56
+ # plugins you already have in `plugins.allow`.
44
57
  openclaw config set 'plugins.allow' '["@latitude-data/openclaw-telemetry"]'
45
58
  ```
46
59
 
47
- ### Step 4 Restart the gateway
60
+ #### 4. Restart the gateway
48
61
 
49
62
  ```bash
50
63
  openclaw gateway restart
@@ -61,11 +74,11 @@ grep -E "blocked|plugin not found|latitude" /tmp/openclaw/openclaw-*.log | tail
61
74
  # → no "blocked", no "plugin not found"
62
75
  ```
63
76
 
64
- Send a message to one of your OpenClaw agents — within a few seconds, traces appear at `https://console.latitude.so/projects/<your-slug>`.
77
+ Send a message to one of your OpenClaw agents — within seconds, traces appear at `https://console.latitude.so/projects/<your-slug>`.
65
78
 
66
- ### Alternative: hand-edit `~/.openclaw/openclaw.json`
79
+ #### Or: hand-edit `~/.openclaw/openclaw.json`
67
80
 
68
- If you'd rather paste a JSON block than run six commands, the equivalent edit is:
81
+ Equivalent to steps 2 + 3 in one paste:
69
82
 
70
83
  ```jsonc
71
84
  {
@@ -88,43 +101,54 @@ If you'd rather paste a JSON block than run six commands, the equivalent edit is
88
101
  }
89
102
  ```
90
103
 
91
- Merge this with whatever else is in `openclaw.json`. Then `openclaw config validate` and `openclaw gateway restart` as above.
104
+ Merge with whatever else is in `openclaw.json`. Then run `openclaw config validate` and `openclaw gateway restart`.
92
105
 
93
- ### Targeting a different environment
106
+ ## Uninstall
94
107
 
95
- For staging or local-dev, also set `baseUrl`:
108
+ If you installed via the CLI:
96
109
 
97
110
  ```bash
98
- # Staging:
99
- openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.baseUrl' "https://staging-ingest.latitude.so"
100
-
101
- # Local dev:
102
- openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.baseUrl' "http://localhost:3002"
111
+ npx -y @latitude-data/openclaw-telemetry-cli@0.0.8 uninstall
103
112
  ```
104
113
 
105
- ### Structural-only telemetry (no content capture)
106
-
107
- If you want trace metadata (timings, token usage, model name, agent name, ids) without the prompt/response content, keep `hooks.allowConversationAccess` at `true` (so OpenClaw still dispatches events to us) and set only `config.allowConversationAccess` to `false`:
114
+ Manual uninstall:
108
115
 
109
116
  ```bash
110
- openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.allowConversationAccess' false
111
- openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].hooks.allowConversationAccess' true
117
+ openclaw plugins uninstall @latitude-data/openclaw-telemetry --force
118
+ openclaw gateway restart
112
119
  ```
113
120
 
114
- Setting `hooks.allowConversationAccess=false` would block dispatch entirely see [The two flags](#the-two-flags). The plugin still emits the full span tree; just the content attributes (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`, tool args/results) are scrubbed. Each span carries a `latitude.captured.content: false` boolean so the gate state is visible in the Latitude UI.
121
+ OpenClaw removes the extension files, install record, plugin entry, and the `plugins.allow` entry.
115
122
 
116
- ## Uninstall
123
+ ## Targeting staging or local dev
124
+
125
+ By default the plugin sends to production (`https://ingest.latitude.so`). Override `baseUrl` to point elsewhere:
117
126
 
118
127
  ```bash
119
- openclaw plugins uninstall @latitude-data/openclaw-telemetry --force
128
+ # Staging
129
+ openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.baseUrl' \
130
+ "https://staging-ingest.latitude.so"
131
+
132
+ # Local dev
133
+ openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.baseUrl' \
134
+ "http://localhost:3002"
120
135
  ```
121
136
 
122
- OpenClaw removes the files, install record, plugin entry, and the `plugins.allow` entry. After that, restart the gateway:
137
+ The CLI handles this with `--staging` / `--dev` flags.
138
+
139
+ ## Structural-only telemetry (no content capture)
140
+
141
+ To get trace metadata (timings, token usage, model name, agent name, ids) without prompt/response content, keep `hooks.allowConversationAccess` at `true` so events still dispatch, and set only `config.allowConversationAccess` to `false`:
123
142
 
124
143
  ```bash
125
- openclaw gateway restart
144
+ openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.allowConversationAccess' false
145
+ openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].hooks.allowConversationAccess' true
126
146
  ```
127
147
 
148
+ Setting `hooks.allowConversationAccess=false` would block dispatch entirely — see [The two flags](#the-two-flags). With this config the plugin still emits the full span tree, just with content attributes (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`, tool args/results) scrubbed. Each span carries `latitude.captured.content: false` so the gate state is visible in the Latitude UI.
149
+
150
+ The CLI handles this with `--no-content`.
151
+
128
152
  ## What gets sent
129
153
 
130
154
  For each agent run, the plugin emits one trace shaped like the actual run:
@@ -145,15 +169,13 @@ agent (root, traceId = hash(runId))
145
169
 
146
170
  Five span kinds:
147
171
 
148
- - **`agent`** — root of the run. Carries `openclaw.session.key`, `openclaw.agent.id`, `openclaw.agent.name`, aggregated token usage across all generations, run duration, success/error status, the first user prompt, and the full final message list. This is where attempt-aggregate `gen_ai.*` lands.
149
- - **`model_call`** — one per actual provider API call inside the run. Carries provider, request/response model, `openclaw.api`, `openclaw.transport`, per-call duration, outcome, error category, time-to-first-byte, request payload bytes, response stream bytes, upstream request id hash, and `gen_ai.input.messages` snapshotted at the moment that generation started. Per-call output messages and per-call token usage aren't surfaced by OpenClaw today (attempt-aggregate only); those stay on `agent`.
172
+ - **`agent`** — root of the run. Carries `openclaw.session.key`, `openclaw.agent.id`, `openclaw.agent.name`, aggregated token usage across all generations, run duration, success/error status, the first user prompt, and the full final message list. Attempt-aggregate `gen_ai.*` lands here.
173
+ - **`model_call`** — one per provider API call inside the run. Carries provider, request/response model, `openclaw.api`, `openclaw.transport`, per-call duration, outcome, error category, time-to-first-byte, request payload bytes, response stream bytes, upstream request id hash, and `gen_ai.input.messages` snapshotted at the moment that generation started. Per-call output messages and per-call token usage aren't surfaced by OpenClaw today (attempt-aggregate only); those stay on `agent`.
150
174
  - **`tool_call:<name>`** — one per tool invocation. Canonical `gen_ai.tool.*` attributes: `name`, `call.id`, `call.arguments`, `call.result`. Sibling of `agent`, NOT child of `model_call` — tools run between generations, not during them.
151
175
  - **`compaction`** — rare; fires when OpenClaw hits the message budget mid-run. Records before/after message counts and the compacted-out count.
152
176
  - **`subagent`** — one per child run spawned by this agent. The child's entire `agent` subtree (its own `model_call`s, `tool_call`s, even further-nested `subagent`s) parents itself underneath via cross-runId trace propagation, so a spawn tree is one waterfall in one trace.
153
177
 
154
- Every span carries `openclaw.agent.id` and `openclaw.agent.name`. Multi-agent setups produce spans tagged with the invoking agent's id, letting you filter and group by agent in the Latitude UI.
155
-
156
- All spans share the same `traceId` so they group as one trace per agent run (and one trace per spawn tree, by virtue of the subagent linkage).
178
+ Every span carries `openclaw.agent.id` and `openclaw.agent.name` — multi-agent setups produce spans tagged with the invoking agent's id, letting you filter and group by agent in the Latitude UI. All spans share the same `traceId`, so they group as one trace per agent run (and one trace per spawn tree, by virtue of the subagent linkage).
157
179
 
158
180
  ### Backend caveat: Codex / Claude-Code-style providers
159
181
 
@@ -173,13 +195,13 @@ We subscribe to OpenClaw's typed plugin hooks (`src/plugins/hook-types.ts` upstr
173
195
 
174
196
  Two more hooks (`llm_input`, `llm_output`) are subscribed to for **content only** — they don't open or close spans, they just enrich the `agent` span with attempt-aggregate data and seed the rolling history snapshot used by per-call `model_call.gen_ai.input.messages`.
175
197
 
176
- The hook system runs handlers fire-and-forget (see [`src/plugins/hooks.ts`](https://github.com/openclaw/openclaw/blob/main/src/plugins/hooks.ts) upstream), so nothing we do here can slow the agent loop. The one exception is `before_tool_call`, which is a `runModifyingHook` — our handler returns `undefined` so OpenClaw dispatches the tool normally. Returning anything else (e.g. `{block: true}`) would block every tool call.
198
+ The hook system runs handlers fire-and-forget, so nothing we do here can slow the agent loop. The one exception is `before_tool_call`, which is a `runModifyingHook` — our handler returns `undefined` so OpenClaw dispatches the tool normally. Returning anything else (e.g. `{block: true}`) would block every tool call.
177
199
 
178
- **No runtime wrapping.** Unlike third-party OpenClaw observability plugins that try to monkey-patch `@mariozechner/pi-ai` (and run into jiti's CJS/ESM module isolation), we stay inside the supported plugin API. The hooks give us everything, at lower risk of breaking on OpenClaw updates.
200
+ **No runtime wrapping.** We stay inside the supported plugin API rather than monkey-patching `@mariozechner/pi-ai`. The hooks give us everything, at lower risk of breaking on OpenClaw updates.
179
201
 
180
202
  ## Configuration reference
181
203
 
182
- The installer writes two blocks to `plugins.entries["@latitude-data/openclaw-telemetry"]`:
204
+ Two blocks live under `plugins.entries["@latitude-data/openclaw-telemetry"]`:
183
205
 
184
206
  ### `.config` — read by the plugin's runtime
185
207
 
@@ -187,8 +209,8 @@ The installer writes two blocks to `plugins.entries["@latitude-data/openclaw-tel
187
209
  | --- | --- | --- | --- |
188
210
  | `apiKey` | yes | — | Bearer token for Latitude ingestion. |
189
211
  | `project` | yes | — | Slug of the project to route traces into. |
190
- | `baseUrl` | no | `https://ingest.latitude.so` | Override OTLP ingest origin. Installer sets this only when you pass `--staging` or `--dev`. |
191
- | `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".** |
212
+ | `baseUrl` | no | `https://ingest.latitude.so` | Override OTLP ingest origin. The CLI sets this only when `--staging` or `--dev` is passed. |
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).** |
192
214
  | `enabled` | no | `true` | Set to `false` to pause emission without uninstalling. |
193
215
  | `debug` | no | `false` | Log diagnostic lines to stderr (visible in the gateway log). |
194
216
 
@@ -205,66 +227,31 @@ The installer writes two blocks to `plugins.entries["@latitude-data/openclaw-tel
205
227
  - **`hooks.*`** is the **dispatch gate**. `false` → OpenClaw never forwards events to us. No traces.
206
228
  - **`config.*`** is the **payload-content gate**. `false` → we emit spans normally but scrub message content from them. Structural-only telemetry.
207
229
 
208
- For *this* plugin, we always couple them — the installer writes both from the same source. If you hand-edit, set them to the same value:
230
+ For *this* plugin we always couple them — the CLI writes both from the same source. If you hand-edit:
209
231
 
210
232
  - **Both `true`**: full content capture (the default).
211
- - **Both `false`**: structural-only telemetry (set via `--no-content`).
212
- - Anything else is incoherent: `hooks: false + config: true` means dispatch is off and content gating is moot; `hooks: true + config: false` works but is what you'd get with both `false` plus extra ceremony.
213
-
214
- ### Environment variable fallbacks
215
-
216
- If a `config.*` key isn't set, the runtime falls back to env vars on the gateway process: `LATITUDE_API_KEY`, `LATITUDE_PROJECT`, `LATITUDE_BASE_URL`, `LATITUDE_DEBUG`, `LATITUDE_OPENCLAW_ENABLED`. The installer doesn't set them — pluginConfig is the canonical surface — but they're useful for flipping `debug` without editing `openclaw.json`.
217
-
218
- ### Manual installation
233
+ - `hooks: true` + `config: false`: structural-only telemetry (set via `--no-content`).
234
+ - `hooks: false` + anything: no traces. Don't.
219
235
 
220
- If you can't run the installer, do exactly what it does:
236
+ ### Environment-variable fallbacks
221
237
 
222
- 1. **Hand placement to OpenClaw** with `openclaw plugins install <path-to-extracted-package> --force`. This is what populates `~/.openclaw/extensions/`, writes the install record, and creates the (initially empty) `plugins.entries[id]` block. Don't hand-place files into the extensions directory the persisted plugin index won't see them.
223
- 2. **Add the config + hooks block** to `~/.openclaw/openclaw.json`:
224
-
225
- ```jsonc
226
- {
227
- "plugins": {
228
- "allow": ["@latitude-data/openclaw-telemetry"],
229
- "entries": {
230
- "@latitude-data/openclaw-telemetry": {
231
- "enabled": true,
232
- "hooks": {
233
- "allowConversationAccess": true
234
- },
235
- "config": {
236
- "apiKey": "lat_xxx",
237
- "project": "my-openclaw-project",
238
- "allowConversationAccess": true
239
- }
240
- }
241
- }
242
- }
243
- }
244
- ```
245
-
246
- Run `openclaw config validate` — should print `valid: true`. Then `openclaw gateway restart`.
238
+ If a `config.*` key isn't set, the runtime falls back to env vars on the gateway process: `LATITUDE_API_KEY`, `LATITUDE_PROJECT`, `LATITUDE_BASE_URL`, `LATITUDE_DEBUG`, `LATITUDE_OPENCLAW_ENABLED`. Useful for flipping `debug` without editing `openclaw.json`.
247
239
 
248
240
  ## Privacy
249
241
 
250
- There are two defaults at play keep them straight:
251
-
252
- - **Installer default**: when you run `npx -y @latitude-data/openclaw-telemetry install` without `--no-content`, the installer writes `allowConversationAccess: true` to both the `hooks` and `config` blocks. **You get full content capture** — prompts, assistant responses, system instructions, tool I/O — shipped to Latitude alongside structural telemetry.
253
- - **Runtime default for absent keys**: if you hand-write `openclaw.json` and leave `allowConversationAccess` out entirely, `config.allowConversationAccess` falls back to `false` at the plugin's runtime (privacy-preserving) and `hooks.allowConversationAccess` falls back to OpenClaw's default (also `false`, which means dispatch is blocked and you get nothing). **Manual installs without those keys produce no traces, not "structural-only traces".**
254
-
255
- The installer always writes both keys to the same value, so the installer-driven path is unambiguous. The runtime-default trap only matters for hand-rolled configs.
242
+ The CLI's first-install default writes `allowConversationAccess: true` to both blocks → full content capture. Pass `--no-content` for structural-only telemetry.
256
243
 
257
- When you pass `--no-content` (or hand-edit both flags to `false`), we emit the same span tree but scrub the content attributes (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`, `gen_ai.tool.call.arguments`/`result`, the interaction's `user_prompt`). Timings, token usage, model name, agent name, ids, and the `latitude.captured.content: false` boolean still flow.
244
+ For hand-edited configs, leaving `allowConversationAccess` out entirely produces **no traces** (not "structural-only traces") because `hooks.allowConversationAccess` defaults to `false` at OpenClaw's level and dispatch is blocked. Always set both keys explicitly.
258
245
 
259
- To pause emission entirely without uninstalling, set `LATITUDE_OPENCLAW_ENABLED=0` in the gateway environment, or set `enabled: false` on the plugin entry in `openclaw.json`.
246
+ To pause emission without uninstalling, set `enabled: false` on the plugin entry, or `LATITUDE_OPENCLAW_ENABLED=0` in the gateway environment.
260
247
 
261
248
  ## Supported OpenClaw versions
262
249
 
263
- Requires OpenClaw **2026.4.25 or newer**. The installer detects the version up-front via `openclaw --version` and aborts with an upgrade message on older versions — supporting the full range with portability shims would mean shipping known-broken behaviour (≤ 2026.4.21 reject `hooks.allowConversationAccess` outright; 2026.4.22 – 2026.4.24 have unverified dispatch gating). Run `npm install -g openclaw@latest` to upgrade.
250
+ 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.
264
251
 
265
252
  ## How it fails
266
253
 
267
- Fail-open by design. If the API is unreachable, your key is wrong, or a hook payload is malformed, the plugin logs to stderr (when `LATITUDE_DEBUG=1`) and the agent run continues unaffected.
254
+ Fail-open by design. If the API is unreachable, your key is wrong, or a hook payload is malformed, the plugin logs to stderr (when `debug: true`) and the agent run continues unaffected.
268
255
 
269
256
  ## License
270
257
 
package/dist/plugin.js CHANGED
@@ -1,7 +1,4 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { arch, hostname, platform, release } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
2
  import { createHash, randomUUID } from "node:crypto";
6
3
  //#region src/client.ts
7
4
  async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs = 1e4 }) {
@@ -88,7 +85,7 @@ function createLogger(debugEnabled) {
88
85
  //#endregion
89
86
  //#region src/otlp.ts
90
87
  const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
91
- const SCOPE_VERSION = readScopeVersion();
88
+ const SCOPE_VERSION = "0.0.8";
92
89
  /** Build an OTLP export request for a single completed agent run. */
93
90
  function buildOtlpRequest(result, options) {
94
91
  const spans = result.spans.map((span) => toOtlpSpan(span, options));
@@ -178,22 +175,6 @@ function safeJson$1(value) {
178
175
  return "";
179
176
  }
180
177
  }
181
- /**
182
- * Runtime-read package version, so OTLP `scope.version` and `service.version`
183
- * always reflect what's actually installed. Read once at module load and
184
- * cached. Falls back to `"unknown"` if the read fails.
185
- *
186
- * Same import.meta.url + ../package.json pattern used by `cli.ts` for the
187
- * `--version` flag — single source of truth in package.json.
188
- */
189
- function readScopeVersion() {
190
- try {
191
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
192
- return JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "unknown";
193
- } catch {
194
- return "unknown";
195
- }
196
- }
197
178
  //#endregion
198
179
  //#region src/messages.ts
199
180
  const ALLOWED_ROLES = new Set([
@@ -448,6 +429,7 @@ var SpanBuilder = class {
448
429
  attrs: {
449
430
  ...flattenCtx(ctx),
450
431
  ...latitudeAttrs(ctx),
432
+ ...sessionAttrs(ctx),
451
433
  "openclaw.run.id": runId,
452
434
  "before_agent_start.prompt:gated": evt.prompt,
453
435
  "before_agent_start.messages:gated": evt.messages ? normalizeMessages(evt.messages) : void 0
@@ -516,6 +498,7 @@ var SpanBuilder = class {
516
498
  endMs: void 0,
517
499
  attrs: {
518
500
  ...latitudeAttrs(ctx),
501
+ ...sessionAttrs(ctx),
519
502
  "openclaw.run.id": evt.runId,
520
503
  "openclaw.call.id": evt.callId,
521
504
  "gen_ai.system": evt.provider,
@@ -573,6 +556,7 @@ var SpanBuilder = class {
573
556
  endMs: void 0,
574
557
  attrs: {
575
558
  ...latitudeAttrs(ctx),
559
+ ...sessionAttrs(ctx),
576
560
  "openclaw.run.id": evt.runId,
577
561
  "gen_ai.tool.name": evt.toolName,
578
562
  "gen_ai.tool.call.id": toolCallId,
@@ -633,6 +617,7 @@ var SpanBuilder = class {
633
617
  endMs: void 0,
634
618
  attrs: {
635
619
  ...latitudeAttrs(ctx),
620
+ ...sessionAttrs(ctx),
636
621
  "openclaw.run.id": runId,
637
622
  "openclaw.compaction.message_count.before": evt.messageCount,
638
623
  "openclaw.compaction.session_file": evt.sessionFile,
@@ -674,6 +659,7 @@ var SpanBuilder = class {
674
659
  endMs: void 0,
675
660
  attrs: {
676
661
  ...latitudeAttrs(ctx),
662
+ ...sessionAttrs(ctx),
677
663
  "openclaw.parent.run.id": parentRunId,
678
664
  "openclaw.run.id": evt.runId,
679
665
  "openclaw.subagent.child_session_key": evt.childSessionKey,
@@ -830,6 +816,21 @@ function flattenCtx(ctx) {
830
816
  };
831
817
  }
832
818
  /**
819
+ * Mirror OpenClaw's session id onto the OTEL-standard keys Latitude's
820
+ * resolver looks for. `gen_ai.session.id` and `session.id` are both in
821
+ * `sessionIdCandidates` (domain/spans/src/otlp/resolvers/identity.ts), so
822
+ * traces can be grouped by session in the Latitude UI without an
823
+ * openclaw-specific code path. Emitted on every span, not just `agent`,
824
+ * so child spans (model_call / tool_call / etc.) inherit the same grouping.
825
+ */
826
+ function sessionAttrs(ctx) {
827
+ if (!ctx.sessionId) return {};
828
+ return {
829
+ "session.id": ctx.sessionId,
830
+ "gen_ai.session.id": ctx.sessionId
831
+ };
832
+ }
833
+ /**
833
834
  * Build `latitude.tags` and `latitude.metadata` attrs from the hook context.
834
835
  * The OTLP encoder JSON-stringifies arrays/objects, which is the encoding
835
836
  * Latitude's resolver expects:
@@ -948,19 +949,25 @@ function registerLatitudePlugin(api, opts = {}) {
948
949
  builder.onLlmOutput(evt, ctx);
949
950
  }));
950
951
  api.on("agent_end", wrap("agent_end", (evt, ctx) => {
951
- const result = builder.onAgentEnd(evt, ctx);
952
- if (!result) {
953
- logger.debug("agent_end fired without a matching run in flight");
954
- return;
955
- }
956
- opts.onEmit?.(result);
957
- const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess });
958
- postTraces({
959
- baseUrl: config.baseUrl,
960
- apiKey: config.apiKey,
961
- project: config.project,
962
- payload,
963
- logger
952
+ queueMicrotask(() => {
953
+ try {
954
+ const result = builder.onAgentEnd(evt, ctx);
955
+ if (!result) {
956
+ logger.debug("agent_end fired without a matching run in flight");
957
+ return;
958
+ }
959
+ opts.onEmit?.(result);
960
+ const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess });
961
+ postTraces({
962
+ baseUrl: config.baseUrl,
963
+ apiKey: config.apiKey,
964
+ project: config.project,
965
+ payload,
966
+ logger
967
+ });
968
+ } catch (err) {
969
+ logger.warn(`agent_end finalize failed: ${String(err)}`);
970
+ }
964
971
  });
965
972
  }));
966
973
  }
@@ -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 { readFileSync } from \"node:fs\"\nimport { arch, hostname, platform, release } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\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\"\nconst SCOPE_VERSION = readScopeVersion()\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/**\n * Runtime-read package version, so OTLP `scope.version` and `service.version`\n * always reflect what's actually installed. Read once at module load and\n * cached. Falls back to `\"unknown\"` if the read fails.\n *\n * Same import.meta.url + ../package.json pattern used by `cli.ts` for the\n * `--version` flag — single source of truth in package.json.\n */\nfunction readScopeVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n const pkgPath = join(here, \"..\", \"package.json\")\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version?: string }\n return pkg.version ?? \"unknown\"\n } catch {\n return \"unknown\"\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 \"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 \"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 \"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 \"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 \"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 * 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 api.on(\n \"agent_end\",\n wrap<OpenClawAgentEndEvent>(\"agent_end\", (evt, ctx) => {\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 }),\n )\n}\n"],"mappings":"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"}
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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"}
@@ -2,18 +2,18 @@
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.6",
5
+ "version": "0.0.8",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": true,
9
9
  "properties": {
10
10
  "apiKey": {
11
11
  "type": "string",
12
- "description": "Latitude bearer token. Required."
12
+ "description": "Latitude bearer token. Needed for the plugin to emit; when absent the plugin stays inactive (it does not error). The installer writes it after `openclaw plugins install` creates the entry, so it is intentionally not schema-required."
13
13
  },
14
14
  "project": {
15
15
  "type": "string",
16
- "description": "Latitude project slug to route traces into. Required."
16
+ "description": "Latitude project slug to route traces into. Needed for the plugin to emit; when absent the plugin stays inactive. Not schema-required for the same reason as apiKey."
17
17
  },
18
18
  "baseUrl": {
19
19
  "type": "string",
@@ -34,7 +34,6 @@
34
34
  "default": true,
35
35
  "description": "Set to false to pause emission without uninstalling."
36
36
  }
37
- },
38
- "required": ["apiKey", "project"]
37
+ }
39
38
  }
40
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latitude-data/openclaw-telemetry",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
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",