Core0 / Nio
Core0
GitHub v2.4.4
Telemetry · Traces

# Traces — one trace per conversation turn

Span hierarchy follows the OTel GenAI semantic conventions wherever a spec equivalent exists; Nio-specific extensions use the `nio.*` prefix. Three span shapes — turn root, tool call, subagent task — covered below with their full attribute schemas.

## Resource attributes

Every Nio provider (tracer / logger / meter) constructs an OTel `Resource` with three identity attributes that flow onto every span, log record, and metric data point that provider emits. Backends surface them as top-level service selectors / filter columns.

Attribute | Value
--- | ---
`service.name` | `nio-<platform>` — one independent service per agent runtime: `nio-claude-code`, `nio-codex`, `nio-hermes`, `nio-openclaw`
`nio.platform` | Raw platform string, same value as the suffix of `service.name` (offered separately so backends that don't expose `service.name` as a queryable attribute still have a filter handle)
`gen_ai.agent.name` | Operator-set `agent_name` from `~/.nio/config.yaml`; absent on the resource when unconfigured (turn span carries a platform-default fallback as a span attribute instead — see the turn-span table below)

Behaviour change in v2.4.2. Earlier releases set `service.name="nio"` for every platform and put `nio.platform` only on individual spans. Existing dashboards filtered on `service.name="nio"` will not match new data — re-target to `service.name=nio-*` (wildcard) or filter on `nio.platform`. Historical traces / logs / metrics keep their original `service.name=nio`.

## Span hierarchy

    Trace: invoke_agent UserPromptSubmit  (root, opens at 1st PreToolUse, ends at Stop / SubagentStop)
      ├─ Span: execute_tool <name>   (PreToolUse → PostToolUse)
      ├─ Span: execute_tool <name>   (...)
      └─ Span: task:execute          (TaskCreated → TaskCompleted, or OpenClaw subagent_spawning → subagent_ended)

## Span: `invoke_agent UserPromptSubmit` — turn root

One per conversation turn. Carries the turn-level metadata: conversation id, accumulated token usage, agent identity, and the redacted user-prompt / assistant-reply previews.

Attribute | Description | Captured at | Platforms
--- | --- | --- | ---
`gen_ai.operation.name` | Constant `invoke_agent` | turn close | all
`gen_ai.provider.name` | Constant `nio` | turn close | all
`gen_ai.conversation.id` | Host session ID | turn close | all
`gen_ai.agent.name` | User-configured `agent_name`; falls back to platform when unset | turn close | all
`session.id` | Mirror of `gen_ai.conversation.id` for OTel base-spec consumers | turn close | all
`gen_ai.usage.input_tokens` | Input tokens consumed across the turn | Stop · SubagentStop · SessionEnd | all
`gen_ai.usage.output_tokens` | Output tokens generated across the turn | Stop · SubagentStop · SessionEnd | all
`gen_ai.usage.cache_creation.input_tokens` | Cache-creation input tokens | Stop · SubagentStop · SessionEnd | all
`gen_ai.usage.cache_read.input_tokens` | Cache-read input tokens | Stop · SubagentStop · SessionEnd | all
`nio.platform` | Source platform — `claude-code` / `hermes` / `openclaw` | turn close | all
`nio.turn_number` | Per-session counter, starts at 1 | turn close | all
`nio.cwd` | Working dir at turn start | turn close (when set) | all
`nio.turn.user_prompt` | First user message of the turn, redacted, ≤2 KB | UserPromptSubmit | all
`nio.turn.assistant_reply` | First assistant reply of the turn, redacted, ≤2 KB | `llm_output` (OpenClaw-native) | OpenClaw only
`nio.turn.cache_hit_rate` | `cache_read / (input + cache_creation + cache_read)`, 0–1 | turn close | all

Token usage source differs by platform. Claude Code: `Stop` reads the transcript JSONL and sums `message.usage` from all assistant entries since turn start. Hermes: same code path as Claude Code if the transcript path is included in the `post_llm_call` payload; otherwise empty. OpenClaw: `llm_output` event payload carries usage directly; accumulated incrementally.

## Span: `execute_tool <name>` — tool span

One per tool invocation. Span name is literally `execute_tool ${toolName || 'unknown'}`. Pre-event opens the span; post-event closes it (with retroactive start time on Claude Code/Hermes since the pre-side process is gone).

Attribute | Description | Captured at | Platforms
--- | --- | --- | ---
`gen_ai.operation.name` | Constant `execute_tool` | PostToolUse | all
`gen_ai.tool.name` | Host tool name (`Bash`, `WebFetch`, …) | PreToolUse · PostToolUse | all
`gen_ai.tool.type` | Tool type, when known | PostToolUse | all
`gen_ai.tool.call.id` | Host tool-call id (`tool_use_id` on Claude Code, `toolCallId` on OpenClaw) | PreToolUse · PostToolUse | all
`gen_ai.tool.call.arguments` | Tool input, redacted, ≤2 KB | PreToolUse | all
`gen_ai.tool.call.result` | Tool output, redacted, ≤2 KB | PostToolUse | all
`nio.tool.error` | Error message when the tool failed | PostToolUse | all
`nio.tool.duration_ms` | Wall-clock tool execution time (ms) — absent on the deny / confirm-denied span (the tool didn't run; use `nio.guard.eval_ms` instead) | PostToolUse | OpenClaw only
`nio.tool.run_id` | OpenClaw-internal run identifier | PreToolUse | OpenClaw only
`nio.tool_summary` | One-line summary derived from tool input | PostToolUse | all
`nio.platform` | Source platform — `claude-code` / `hermes` / `openclaw` | PostToolUse | all
`nio.turn_number` | Parent turn's number | PostToolUse | all
`nio.cwd` | Working dir at hook fire | PostToolUse (when set) | all
`nio.guard.decision` | Guard verdict — `allow` / `deny` / `confirm_allowed` / `confirm_denied` | PreToolUse | all
`nio.guard.risk_level` | Guard risk level — `low` / `medium` / `high` / `critical` / `unknown` | PreToolUse | all
`nio.guard.risk_score` | Guard risk score, 0–1 | PreToolUse | all
`nio.guard.risk_tags` | Comma-joined rule IDs that fired | PreToolUse | all
`nio.guard.phase_stopped` | Phase that produced the verdict (`0` = tool-gate, `1`–`6` = runtime pipeline) | PreToolUse | all
`nio.guard.top_finding_rule` | `rule_id` of the highest-ranked finding (when any fired) | PreToolUse | all
`nio.guard.eval_ms` | Wall-clock cost of the guard evaluation (ms) | PreToolUse | all

Span status: `ERROR` (with `recordException(error)`) when the tool failed or the guard denied / confirm-denied; `OK` otherwise.

Deny / confirm-denied spans. When the guard blocks a tool, `PostToolUse` never fires — so the span is emitted synchronously by the same process that ran the guard (the `guard-hook.ts` PreToolUse on Claude Code / Codex; the `hook-cli.ts` `pre_tool_call` branch on Hermes; the `before_tool_call` handler in the OpenClaw plugin). Span name is the same `execute_tool <tool>` as the allow path — the discrimination is on `nio.guard.decision` + ERROR status + the reason in the exception message. Wall-clock starts at the real `evalStartMs` so the span duration reflects the guard window, and `nio.guard.eval_ms` carries the same value as an explicit attribute for filtering.

## Span: `task:execute` — task span

One per subagent dispatch. Opens at `TaskCreated` (Claude Code, Teammates / cloud-agent flows) or `subagent_spawning` (OpenClaw); closes at the matching completion event.

Attribute | Description | Captured at | Platforms
--- | --- | --- | ---
`nio.task_id` | Task id from the dispatch event | TaskCreated | Claude Code + OpenClaw
`nio.task_summary` | Derived from task input (Claude Code: `task_input.prompt`; OpenClaw: empty) | TaskCreated | Claude Code + OpenClaw
`nio.platform` | Source platform — `claude-code` / `openclaw` | TaskCompleted | Claude Code + OpenClaw
`nio.session_id` | Host session id | TaskCompleted | Claude Code + OpenClaw
`nio.turn_number` | Parent turn's number | TaskCompleted | Claude Code + OpenClaw
`nio.cwd` | Working dir at task start | TaskCompleted | Claude Code + OpenClaw

Known gap · not yet GenAI-aligned

Span name is the literal `task:execute` (not `execute_tool task`); session id uses `nio.session_id` instead of `gen_ai.conversation.id` + `session.id`. The other two spans use GenAI semantic conventions; the task span is intentionally on the legacy schema until Claude Code and OpenClaw can migrate in lockstep.

## Trace state lifecycle

Claude Code and Hermes spawn a fresh node process per hook event, so a `PreToolUse` in process A and the matching `PostToolUse` in process B can't share an OTEL `Span` object. Both platforms bridge state via an on-disk cache keyed by session id; pending spans land there at pre-event time and get materialised retroactively at post-event time with the original start timestamp. OpenClaw runs as a single daemon, so the equivalent state lives in an in-memory `Map<sessionId, State>` instead. All three platforms route through the same trace-collector helper functions — span names and attribute keys are identical regardless of where the state was kept.