> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Event model

> How telemetry events are identified, scoped, filtered, and aggregated — and the full signal reference.

Telemetry events are structured records associated with run identity. The store can derive a
logical lineage path from that identity; lineage does not imply runtime filesystem fork. Organization and
project are assigned from authenticated request context at ingest time.

## Core fields

| Field          | Meaning                                                                                                                                                                                                           |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `run_id`       | Captured agent run. Every event carries one.                                                                                                                                                                      |
| `lineage_path` | Dotted ltree of run ULIDs from the tree root to this run (e.g. `01H8A.01H8B.01H8C`), derived from the run. Lexicographically sortable and prefix-addressable, so a subtree scan is a `lineage_path` prefix match. |
| `root_run_id`  | Root run of this run's tree, derived from the run.                                                                                                                                                                |
| `signal`       | Event family — see the [signal reference](#signals) below.                                                                                                                                                        |
| `name`         | Event name within the signal.                                                                                                                                                                                     |
| `attributes`   | Typed scalar key/value map.                                                                                                                                                                                       |
| `payload_ref`  | Optional digest reference to a large payload stored out of line.                                                                                                                                                  |
| `event_id`     | Stable event id used for idempotent ingest and deduplication.                                                                                                                                                     |

## Time

Events use a hybrid logical clock:

| Field        | Meaning                              |
| ------------ | ------------------------------------ |
| `ts_wall_ns` | Wall-clock time in nanoseconds.      |
| `ts_logical` | Logical counter for causal ordering. |

## Signals

Every event belongs to one **signal** — the family of thing it records. Two kinds of family land in
the same stream:

* **Captured signals** record what your workload did: what it ran, printed, and requested. They
  flow for a command wrapped by [`hiloop run`](/guides/capturing-telemetry) and for supported
  boundaries in a sandbox's managed capture session.
* **Platform signals** record what the platform did around the workload: lifecycle, scheduling,
  network-policy decisions. They carry resource identities and state vocabulary — never command
  lines, bodies, or output — so they flow for every sandbox regardless of its capture setting.

| Signal                                          | Records                                                                          | Capture consent  |
| ----------------------------------------------- | -------------------------------------------------------------------------------- | ---------------- |
| [`llm`](#llm-and-net-http-exchanges)            | Model-provider HTTP exchanges (and harness spans carrying `gen_ai.*` attributes) | respected        |
| [`net`](#llm-and-net-http-exchanges)            | Every other HTTP exchange the workload makes                                     | respected        |
| [`log`](#log-process-output)                    | The workload's stdout/stderr (and the harness's own OpenTelemetry logs)          | respected        |
| [`exec`](#exec-command-lifecycle)               | Command lifecycle: what ran, its exit code, its duration                         | respected        |
| [`span`](#span-harness-traces)                  | The harness's own OpenTelemetry spans                                            | respected        |
| [`metric`](#metric-resource-samples)            | Resource utilization samples from an explicitly enabled producer                 | producer-defined |
| [`egress`](#egress-network-policy-decisions)    | Network egress policy decisions — allowed, blocked, warned                       | always flows     |
| [`runtime`](#runtime-platform-lifecycle)        | Sandbox state transitions, operation lifecycle, and queue latency                | always flows     |
| [`annotation`](#annotation-knowledge-you-write) | Structured knowledge you write against runs and events                           | you write it     |

### `llm` and `net` — HTTP exchanges

Each HTTP exchange the workload makes becomes a pair of events — `http.request` then
`http.response` — sharing an `http.exchange_id`. Model-provider exchanges are the `llm` signal;
every other exchange is `net`. The shapes are identical:

| Event           | Attributes                                                                                 |
| --------------- | ------------------------------------------------------------------------------------------ |
| `http.request`  | `http.exchange_id`, `http.method`, `http.host`, `http.target`, `http.request.content_type` |
| `http.response` | `http.exchange_id`, `http.status_code`                                                     |

Ordinary bodies are captured up to a size cap, scrubbed of [recognized credential
patterns](/observability/redaction) by default, and stored out of line under `payload_ref` — fetch
them with [`hiloop events payload`](/reference/cli/hiloop). A local `hiloop run --no-redact`
disables only that generic scan. Exact supported OAuth token exchanges are metadata-only instead.
Content type and content encoding are the only header values recorded; no other header —
`Authorization`, `Cookie` and the rest — is captured. `http.target` is ordinarily the full request
target, while exact OAuth omission also removes its query values. A harness OpenTelemetry span that
carries `gen_ai.*` or `llm.*` attributes also lands under `llm` (named by the span); span attributes
are stored as sent and are not scrubbed.

### `log` — process output

One event per captured chunk of process output:

| Event            | Attributes                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `process.stdout` | `stream`, `message` (small chunks inline; larger ones under `payload_ref`)                                                      |
| `process.stderr` | `stream`, `message`                                                                                                             |
| `process.stdin`  | `stream`, `message` — input injected into a steered run or interactive execution                                                |
| `capture.drain`  | `capture.complete`, `capture.blobs.*`, `capture.events.*`, optional `capture.error` — the run-end capture-health record (below) |

The harness's own OpenTelemetry log records land under `log` too, named by their log event name
(the severity text when unnamed), with the body under `message`.

**Capture health.** Capture is best-effort by design — it can never fail your workload — so it
reports its own completeness instead: as a captured run drains, one final `capture.drain` event
states whether everything captured actually landed. `capture.complete` is the verdict — `true`
only when every uploadable payload blob landed and no exported event was lost. Behind it:
`capture.blobs.found` / `capture.blobs.landed` / `capture.blobs.missing` /
`capture.blobs.missing_bytes`, with `capture.blobs.oversize` counting bodies over the upload cap
that stayed local; and `capture.events.dropped` / `capture.events.rejected` /
`capture.events.pending` when events were spooled through a gateway outage (`pending` records late
delivery, never loss — the record queues behind its own backlog, so its arrival certifies
everything spooled before it landed too). `capture.error` carries the failure when a drain went
wrong. A captured run with no `capture.drain` event at all ended without draining — treat its
capture as incomplete.

### `exec` — command lifecycle

The lifecycle of the commands your workload ran, from two vantage points that share one signal.

A command wrapped by [`hiloop run`](/guides/capturing-telemetry) records its own process boundary:

| Event            | Attributes                                                                                                                                                        |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `process.start`  | `process.command_args` (the full argv, JSON-encoded), `process.cwd`, `process.pid`, `process.env_allowlist` (variable **names** only — values are never captured) |
| `process.exit`   | `process.exit_code`, `process.duration_ms`, `process.term_signal` (when signal-killed)                                                                            |
| `process.signal` | `signal` — a signal delivered to the process group (e.g. steering a run)                                                                                          |

Every event one `hiloop run` invocation captures — lifecycle, output, HTTP exchanges, and its
run-end capture-health record — also carries `wrapper.invocation_id`, a unique id minted per
invocation, so one wrapped process's events stay correlated even when several wraps share a run.

A buffered [sandbox execution](/sandboxes/commands-executions) is observed by the platform from the
host side, so the record exists even if the process never starts:

| Event        | Attributes                                                                                                                                                                                                |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `exec.start` | `execution.id`, `sandbox.id`, `process.command`, `process.command_args` (JSON array)                                                                                                                      |
| `exec.end`   | `execution.id`, `sandbox.id`, `process.exit_code`, `process.duration_ms` — or, when the execution failed before producing a process result, `exec.error` (a stable error code) plus `process.duration_ms` |

Every emitted `exec.start` gets exactly one `exec.end`, so a failed execution never reads as
still-running. These events carry workload command lines, so they respect the sandbox's capture
setting. The command's environment (a secret carrier), stdin, and captured output are never
attached — output lives in [execution artifacts](/sandboxes/commands-executions) and `log` events.

### `span` — harness traces

If the wrapped agent emits its own OpenTelemetry spans, each span becomes one event named after the
span, with the trace identity under `otel.trace_id`, `otel.span_id`, and `otel.parent_span_id`
(promoted to the `trace_id`/`span_id`/`parent_span_id` [columns](#promoted-columns)). Spans
carrying `gen_ai.*`/`llm.*` attributes classify as `llm` instead.

### `metric` — resource samples

`hiloop run --sample-resources` can emit best-effort `resource.sample` events roughly every 15
seconds for a local process tree. Authoritative runtime-owned resource accounting will use the same
event shape when it ships.

| Attribute                                     | Meaning                                                                                                                     |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `resource.source`                             | `process_tree` for the current best-effort local producer; future runtime-owned producers must use a distinct source token. |
| `cpu.usage_pct`, `mem.rss_mb`, `disk.used_mb` | CPU, resident memory, and disk usage                                                                                        |
| `gpu.<n>.util_pct`, `gpu.<n>.mem_mb`          | Per-GPU utilization and memory, when GPUs are attached                                                                      |

See [usage metering](/guides/usage-metering) for turning samples into utilization and cost answers.

### `egress` — network policy decisions

The schema reserves `egress` events for future runtime-native destination-policy decisions. The
current fixed network floor emits none of the connection-level events below; the retired shared
proxy's SNI audit stream is not a current product surface.

| Reserved event   | Intended meaning                                              |
| ---------------- | ------------------------------------------------------------- |
| `egress.allowed` | The connection matched the policy and was spliced through.    |
| `egress.blocked` | A future destination policy refused the connection.           |
| `egress.warn`    | Warn mode — allowed but flagged (observe before you enforce). |

| Attribute                                                      | Meaning                                                                                                                                                                                                                                     |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `egress.decision_id`                                           | Unique id for this decision, stable across delivery retries.                                                                                                                                                                                |
| `egress.sni`                                                   | The destination name from the TLS handshake.                                                                                                                                                                                                |
| `egress.decision`, `egress.rule_matched`, `egress.enforcement` | What was decided, which rule matched, and the enforcement mode.                                                                                                                                                                             |
| `egress.dst_ip`                                                | The IP actually dialed — with `egress.sni`, joins a decision query-side to the HTTP exchange it gated.                                                                                                                                      |
| `egress.bytes`                                                 | Bytes transferred.                                                                                                                                                                                                                          |
| `egress.executing_kind`, `egress.executing_principal`          | The [identity the sandbox executes as](/concepts/workload-identity) (`user`, `service_account`, or `workload`) and its stable principal id, when stamped — so "which workload was blocked reaching this destination" is a one-query answer. |
| `sandbox.id`                                                   | The sandbox the connection came from.                                                                                                                                                                                                       |

When a runtime-native producer ships, these events remain platform metadata and must never carry
credential values or request bodies.

### `runtime` — platform lifecycle

What the platform did to a sandbox and when: state transitions and operation lifecycle with queue
latency. These are control-plane metadata with no workload content,
so they flow for every sandbox regardless of its capture setting. Emission is best-effort and
always follows the committed change, so telemetry can never affect an operation's outcome.

| Event                   | Attributes                                                                                                                      |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `sandbox.created`       | `sandbox.id`                                                                                                                    |
| `sandbox.state_changed` | `sandbox.id`, `state.from`, `state.to`, optional `reason` (`reconcile` marks drift the platform observed rather than requested) |
| `sandbox.preempted`     | `sandbox.id`, `state.from`, `state.to` — the platform observed an eviction it did not request                                   |
| `sandbox.deleted`       | `sandbox.id`                                                                                                                    |
| `operation.started`     | `operation.id`, `operation.kind`, `attempt`, `queue_wait_ms` (total time the operation waited to run, across retries)           |
| `operation.succeeded`   | `operation.id`, `operation.kind`, `attempt`                                                                                     |
| `operation.failed`      | `operation.id`, `operation.kind`, `attempt` (absent when the operation was settled by the deadline sweep), `error.code`         |
| `operation.rescheduled` | `operation.id`, `operation.kind`, `attempt`                                                                                     |

**Which run they land on.** An operation's events ride the run of the sandbox it targets, and an
execution's ride the run of its owning sandbox. A resource with no run emits nothing.

**Deliberate exclusions.** Periodic reconciliation emits no `operation.*` events — that would flood
every active run with platform noise; what surfaces is the drift a reconcile actually commits
(`sandbox.state_changed` with `reason=reconcile`, or `sandbox.preempted`). Background cleanup maps
to no run and emits nothing.

### `annotation` — knowledge you write

[Annotations](/observability/annotations) are ordinary events: the `name` is the annotation schema
name, and your payload lives in the attributes (promoted fields become typed columns in the
schema-named `ann_*` view). They're the write path for eval scores, verdicts, and any other
structured knowledge you attach to runs.

## Correlation keys

Attributes that join events to each other and to resources:

| Key                              | Joins                                                                                                                                       |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `http.exchange_id`               | An exchange's `http.request` to its `http.response`.                                                                                        |
| `execution.id`                   | `exec.start` to `exec.end`, so one command's start and end line up.                                                                         |
| `operation.id`                   | An operation's lifecycle events to each other: `operation.started`, `operation.succeeded`, `operation.failed`, and `operation.rescheduled`. |
| `wrapper.invocation_id`          | Every event one `hiloop run` invocation captured — lifecycle, output, exchanges, capture health — even when several wraps share a run.      |
| `sandbox.id`                     | Platform events to the sandbox resource.                                                                                                    |
| `otel.trace_id` / `otel.span_id` | Harness spans to each other and to your external tracing backend.                                                                           |
| `egress.decision_id`             | One egress decision, uniquely — pair with `egress.sni`/`egress.dst_ip` to find the exchange it gated.                                       |

## Promoted columns

The query engine promotes common network and tracing fields into typed columns — for example
`http_status_code`, `http_host`, `trace_id`, and `span_id`.

Use promoted columns when you filter or aggregate on them. Everything else an event carries lives
in `attributes_json` (reach in with `hiloop_json_get`), and model-shaped fields (model, token
counts, messages) derive from the captured payload bodies through a
[data view](/observability/data-views) like `otel_genai_calls`.

See [the concept page](/concepts/event-model) for the full allowlist.
