github latitude-dev/latitude-llm openclaw-telemetry-0.0.6
OpenClaw Telemetry v0.0.6

6 hours ago

Re-publish of the never-shipped 0.0.5 with two install-blocking fixes: the runtime no longer reads process.env, and the CLI is no longer in this package. Both were tripping OpenClaw 2026.4.25's openclaw plugins install security scan (the env-harvesting rule flagged the env-read + fetch combo in the runtime, and the dangerous-exec rule flagged the CLI's child_process.spawn call). The runtime ships clean now; the one-shot installer is moving to a separate @latitude-data/openclaw-telemetry-cli package and will land in a follow-up.

This release otherwise inherits everything that was queued for 0.0.5 — the span tree redesign, per-call attributes on model_call, subagent trace propagation, latitude.tags / latitude.metadata enrichment, and the privacy :gated attribute mechanism.

Changed (install path)

  • process.env fallback removed from loadConfig. loadConfig now reads only the per-plugin config bucket OpenClaw passes via api.pluginConfig (i.e. the user's plugins.entries[id].config block). Earlier 0.0.x versions also fell back to env vars when keys were missing — the OpenClaw 2026.4.25 install scanner flags any runtime source that combines process.env reads with a network-send call (we have fetch( in postTraces), so the fallback was tripping installs even though the installer always wrote credentials to the config bucket anyway. For dev-time testing, set config.debug = true in openclaw.json directly.
  • The latitude-openclaw CLI is no longer in this package. Installation is now a manual flow: openclaw plugins install @latitude-data/openclaw-telemetry, then five openclaw config set calls (or one hand-edit of openclaw.json), then openclaw gateway restart. The README documents both. A one-shot installer is coming back as a separate package — npx -y @latitude-data/openclaw-telemetry-cli install — that doesn't go through openclaw plugins install and so isn't subject to the install-time scanner. Tracking that work in the follow-up to this PR.

Removed

  • bin field, ./cli export entry, and @clack/prompts + picocolors dependencies from package.json. cli.ts from the bundle entry list. The package now publishes only dist/plugin.js + dist/plugin.d.ts + openclaw.plugin.json.
  • process.env read in loadConfig (and the surrounding fallback comment, rewritten to avoid the literal string the scanner regex matches on in source).

Added

  • Regression test that grep-asserts no process.env appears in any runtime src/*.ts file. Same shape as the intercept-module guard in claude-code-telemetry.

Span tree redesign (carried over from the queued 0.0.5)

The plugin now emits spans that match the actual structure of an OpenClaw agent run instead of collapsing every generation + tool into a single fake llm_request. Existing dashboards keyed on gen_ai.* attribute names still work — span names changed, attribute namespaces didn't.

Changed (breaking, in trace shape)

  • One agent span per run, with model_call / tool_call / compaction / subagent children. The old shape had a single interaction (renamed from agent) with one llm_request covering the whole attempt and tool spans as siblings. That was wrong on two counts: llm_input/llm_output fire ONCE per attempt (not per generation), and an attempt is a sequence of generations interleaved with tool executions. The new shape:

    agent (root, traceId = hash(runId))
    ├─ compaction         (0..1, rare; budget-triggered)
    ├─ model_call         (1..N, one per provider API call)
    ├─ tool_call: foo     (between model_calls; sibling of agent)
    ├─ model_call
    ├─ tool_call: bar
    ├─ subagent           (0..N — nested child agent spans land underneath)
    └─ model_call
    

    Tool spans are siblings of agent, not children of model_call, because tools run between generations — not during them.

  • Span names follow OpenClaw's events, not OTel semantic-convention terms. agent / model_call / tool_call / compaction / subagent. Attribute namespaces stay gen_ai.* and openclaw.*.

  • Per-call attributes on model_call spans. Each generation gets its own duration, outcome, error category, upstream request id hash, time-to-first-byte, request payload bytes, response stream bytes — straight from model_call_started / model_call_ended payloads.

  • Per-call input messages on model_call spans via the snapshot trick. gen_ai.input.messages on each model_call reflects what THAT generation actually saw — the rolling history evolves across the run as before_tool_call appends synthetic assistant tool_call parts and after_tool_call appends tool responses. Per-call output messages and per-call usage aren't surfaced by OpenClaw today (they're attempt-aggregate); those stay on the agent span only, with a README pointer to the upstream feature request.

  • Subagent spans nest the child's full agent tree underneath via cross-runId trace propagation. When subagent_spawned fires we register a Map<childRunId, parentTraceId+subagentSpanId> link. The child's before_agent_start consults the map, uses the parent's traceId, and parents the child agent under the parent's subagent span. Same trace, one waterfall across the spawn tree.

Added

  • New typed-hook subscriptions: before_agent_start, model_call_started, model_call_ended, before_compaction, after_compaction, subagent_spawned, subagent_ended.
  • Privacy gating implemented via :gated attribute key suffix. The OTLP encoder strips any attribute whose key ends in :gated when allowConversationAccess === false — uniform mechanism, no per-key conditional. Gated attributes: gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions, user_prompt, gen_ai.tool.call.arguments, gen_ai.tool.call.result, before_compaction.messages, before_agent_start.{prompt,messages}, agent_end.messages, and openclaw.error.message (error strings can leak prompt/response content).
  • Abandoned-span handling: any model_call / tool_call / compaction / subagent open at agent_end is force-closed with outcome: "abandoned" so trace gaps don't appear when an attempt errors mid-flight.
  • latitude.tags and latitude.metadata enrichment attributes on every emitted span. Tags is a JSON-encoded string array; metadata is a JSON-encoded string object. Both are populated from per-run state and surface in the Latitude UI for filtering and grouping. Empty values are omitted so spans stay compact.

Removed

  • src/turn-builder.ts + src/turn-builder.test.ts — replaced by src/span-builder.ts / src/span-builder.test.ts. The state machine is fundamentally different (multiple open spans per run instead of a single RunRecord with LlmCallRecord[]).
  • Old interaction and llm_request span names — folded into agent and split per-generation as model_call.
  • orphanTools logic — no longer needed once tools are paired with proper before/after events. Still-open tools at agent_end now go through the abandoned-span path.

Notes for operators

  • Existing OpenClaw versions (≥ 2026.4.25) ship model_call_started / model_call_ended already; the minimum-version requirement is unchanged.
  • Codex/Claude-Code style backends will still show one model_call per attempt because their internal generations aren't surfaced to OpenClaw's selection layer. README now documents this. Filing the OpenClaw-side enhancement (per-call usage + assistantText on model_call_ended) is upstream and out of scope here.
  • The before_tool_call hook is a runModifyingHook. Plugin handler returns undefined (so OpenClaw dispatches the tool normally). New regression test verifies the return is undefined.

Don't miss a new latitude-llm release

NewReleases is sending notifications on new releases.