@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 +21 -157
- package/README.md +80 -93
- package/dist/plugin.js +40 -33
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +4 -5
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,157 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
10
|
+
## Install
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
### Recommended — one-shot CLI
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
The companion CLI handles every step (install, config, validate, restart) in one command:
|
|
12
15
|
|
|
13
16
|
```bash
|
|
14
|
-
|
|
17
|
+
npx -y @latitude-data/openclaw-telemetry-cli@0.0.8 install
|
|
15
18
|
```
|
|
16
19
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
48
|
+
Both `allowConversationAccess` writes are required — see [The two flags](#the-two-flags).
|
|
36
49
|
|
|
37
|
-
|
|
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
|
|
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.
|
|
43
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
79
|
+
#### Or: hand-edit `~/.openclaw/openclaw.json`
|
|
67
80
|
|
|
68
|
-
|
|
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
|
|
104
|
+
Merge with whatever else is in `openclaw.json`. Then run `openclaw config validate` and `openclaw gateway restart`.
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
## Uninstall
|
|
94
107
|
|
|
95
|
-
|
|
108
|
+
If you installed via the CLI:
|
|
96
109
|
|
|
97
110
|
```bash
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
111
|
-
openclaw
|
|
117
|
+
openclaw plugins uninstall @latitude-data/openclaw-telemetry --force
|
|
118
|
+
openclaw gateway restart
|
|
112
119
|
```
|
|
113
120
|
|
|
114
|
-
|
|
121
|
+
OpenClaw removes the extension files, install record, plugin entry, and the `plugins.allow` entry.
|
|
115
122
|
|
|
116
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
149
|
-
- **`model_call`** — one per
|
|
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
|
|
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
|
|
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.**
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
-
|
|
212
|
-
-
|
|
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
|
-
|
|
236
|
+
### Environment-variable fallbacks
|
|
221
237
|
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 `
|
|
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 =
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
}
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["safeJson"],"sources":["../src/client.ts","../src/config.ts","../src/logger.ts","../src/otlp.ts","../src/messages.ts","../src/span-builder.ts","../src/plugin.ts"],"sourcesContent":["import type { Logger } from \"./logger.ts\"\nimport type { OtlpExportRequest } from \"./types.ts\"\n\nexport async function postTraces({\n baseUrl,\n apiKey,\n project,\n payload,\n logger,\n timeoutMs = 10_000,\n}: {\n baseUrl: string\n apiKey: string\n project: string\n payload: OtlpExportRequest\n logger: Logger\n timeoutMs?: number\n}): Promise<void> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/v1/traces`\n const bodyText = JSON.stringify(payload)\n logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`)\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"X-Latitude-Project\": project,\n },\n body: bodyText,\n signal: controller.signal,\n })\n if (!res.ok) {\n const text = await res.text().catch(() => \"\")\n logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`)\n } else {\n logger.debug(`ingest HTTP ${res.status}`)\n }\n } catch (err) {\n logger.warn(`ingest failed: ${String(err)}`)\n } finally {\n clearTimeout(timer)\n }\n}\n","export interface Config {\n apiKey: string\n baseUrl: string\n project: string\n enabled: boolean\n debug: boolean\n /**\n * When false, the plugin still emits one span per LLM call / tool / run, but\n * scrubs raw conversation content (input/output messages, system prompt,\n * tool args, tool results, the surfaced first-prompt). Token counts, model\n * names, agent ids, and timings are unaffected.\n */\n allowConversationAccess: boolean\n}\n\nconst DEFAULT_BASE_URL = \"https://ingest.latitude.so\"\n\n/**\n * Build a `Config` from OpenClaw's per-plugin config bucket. The plugin SDK\n * passes `api.pluginConfig` (the user's `plugins.entries[id].config` block)\n * to the registration function — that's the only source.\n *\n * Earlier 0.0.x versions also fell back to environment variables when keys\n * were missing from pluginConfig. That fallback is gone deliberately:\n * OpenClaw 2026.4.25's `openclaw plugins install` runs a static-analysis\n * security scan that flags any runtime source combining environment-variable\n * access with a network-send call (we have `fetch(` in postTraces). With\n * the fallback our bundled runtime tripped the scanner. The installer\n * writes credentials to `plugins.entries[id].config` anyway, so the\n * fallback was polish-not-feature — its removal also gives a cleaner\n * privacy story (the runtime can't pick up credentials the operator\n * didn't put in openclaw.json).\n *\n * For dev-time testing with debug logs, set `config.debug = true` in\n * openclaw.json directly.\n */\nexport function loadConfig(pluginConfig: Record<string, unknown> | undefined = undefined): Config {\n const fromOpts = pluginConfig ?? {}\n\n const apiKey = pickString(fromOpts.apiKey) ?? \"\"\n const project = pickString(fromOpts.project) ?? \"\"\n const baseUrl = pickString(fromOpts.baseUrl) ?? DEFAULT_BASE_URL\n\n const debug = pickBool(fromOpts.debug) ?? false\n const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false\n\n const explicitlyDisabled = pickBool(fromOpts.enabled) === false\n const hasCreds = apiKey !== \"\" && project !== \"\"\n\n return {\n apiKey,\n baseUrl,\n project,\n debug,\n allowConversationAccess,\n enabled: hasCreds && !explicitlyDisabled,\n }\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined\n}\n\nfunction pickBool(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined\n}\n","const PREFIX = \"[latitude-openclaw]\"\n\nexport interface Logger {\n debug: (msg: string) => void\n warn: (msg: string) => void\n}\n\nexport function createLogger(debugEnabled: boolean): Logger {\n return {\n debug: debugEnabled ? (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`) : () => {},\n warn: (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`),\n }\n}\n","import { 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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"}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|