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

# OpenClaw Plugin: Hook-Based Runtime Instrumentation

> The Critiqor OpenClaw plugin hooks into tool lifecycle and extension API events to capture runtime evidence without modifying agent code or parsing logs.

The Critiqor OpenClaw plugin is a passive observer. It is registered through `definePluginEntry()` from the OpenClaw plugin SDK, which exposes a `register(api)` callback that fires when OpenClaw loads the plugin. Inside `register`, the plugin calls `safeSubscribe(api, eventType, sourceLayer)` for every event type it cares about, which in turn calls `api.on(eventType, handler)` to attach listeners for both timeline events and tool lifecycle events. Those listeners normalize each incoming event and append it to `runs/<run_id>/session.json`. The plugin does **not** score the agent, make any diagnosis judgments, render output, or modify behavior in any way. All scoring and diagnosis happen later in the Python observation engine when `critiqor finalize` is called.

The plugin is bundled inside the Critiqor package at `critiqor/clawhub/critiqor-openclaw/index.js` and is auto-loaded by OpenClaw via the `OPENCLAW_BUNDLED_PLUGINS_DIR` environment variable that the supervised runtime sets before spawning the child process.

## What the Plugin Collects

The plugin registers listeners across two collection layers:

| Layer         | API                          | Events Captured                                                                                                                                                                                                                                        |
| ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Extension API | `api.on(eventType, handler)` | `agent_start`, `agent_end`, `turn_start`, `turn_end`, `session_start`, `session_end`, `before_provider_request`, `after_provider_response`, `message_received`, `message_sent`, `message_start`, `message_update`, `message_end`, `input`, `user_bash` |
| Tool Hooks    | OpenClaw tool lifecycle      | `tool_call`, `tool_result`, `tool_execution_start`, `tool_execution_update`, `tool_execution_end`                                                                                                                                                      |

Each listener is registered with `safeSubscribe()`, which silently ignores unsupported event names. This makes the plugin forward- and backward-compatible: it works against older OpenClaw builds that do not expose every timeline alias without throwing or crashing.

## Event Normalization

Every captured event passes through `normalizeEvent()` before being written to disk. Normalization does the following:

* **Timestamps every event** with the current UTC time in ISO 8601 format via `nowIso()`.
* **Tracks `tool_call_id` for correlation** by extracting it from `payload.toolCallId` or `payload.tool_call_id`. This ID ties a `tool_call` to its eventual `tool_result` or `tool_execution_end`.
* **Computes `duration_ms` for tool calls** using a `START_TIME_BY_TOOL_CALL` map. When a `tool_call` or `tool_execution_start` event arrives, the current timestamp is stored keyed by `tool_call_id`. When the corresponding `tool_result` or `tool_execution_end` arrives, `duration_ms` is computed as `Date.now() - startedAt` and the entry is cleared from the map.
* **Sets `status: "error"`** when `payload.isError === true`; otherwise sets `status: "ok"`.
* **Adds a `source_layer` tag** to every event: `"extension_api"` for timeline events or `"tool_hooks"` for tool lifecycle events. The observation engine uses this tag to distinguish event origins during finalization.

## Memory Event Handling

When a tool hook fires for a tool named `memory_search` or `memory_get`, the plugin writes the normalized event to `session.json` **twice**: once with its original `event_type` (e.g. `tool_call`) and a second time with `event_type` overridden to `memory_search` or `memory_get` and `payload.observed_as` set to the original type.

This re-emission gives the Python diagnosis engine a dedicated stream of memory-specific events to inspect when checking for memory degradation. Without it, memory tool calls would be indistinguishable from any other tool call in the event log.

## Session File Writing

Each event is appended to `runs/<run_id>/session.json` by `updateSessionSummary()`. The file is read, updated, and rewritten atomically on every event. The session file maintains running metrics alongside the event array so the state of the session is always readable mid-run:

```json theme={null}
{
  "session_id": "run_001",
  "run_id": "run_001",
  "schema_version": "critiqor.session.v1",
  "created_at": "2025-06-01T12:00:00.000Z",
  "updated_at": "2025-06-01T12:00:01.234Z",
  "events_file": "session.json",
  "events": [ "..." ],
  "metrics": {
    "total_events": 12,
    "by_event_type": { "tool_call": 5, "tool_result": 5, "turn_start": 1, "turn_end": 1 },
    "by_source_layer": { "tool_hooks": 10, "extension_api": 2 },
    "error_events": 0,
    "tool_calls": { "read_file": 3, "bash": 2 }
  }
}
```

The `metrics` block is updated on every write, so `total_events`, `by_event_type`, `by_source_layer`, `error_events`, and `tool_calls` counts always reflect the full set of events collected so far.

## Instrumentation vs. Log Parsing

The plugin hooks directly into OpenClaw's runtime event system. It does **not** tail log files, read stdout, or parse any text output. This design choice has three concrete consequences:

* **Every event is structured from the start.** There is no text-to-JSON parsing step and no risk of partial lines, truncated output, or encoding issues producing malformed events.
* **Duration is precise.** `duration_ms` is computed from the actual wall-clock timestamps at the `tool_execution_start` and `tool_execution_end` hook boundaries — not estimated from log timestamps that may be buffered or delayed.
* **No events are missed.** Log-based approaches can drop events due to log rotation, buffer flushes, or high-throughput races. Hook-based capture fires synchronously with the runtime, so the event record is complete even for very short-lived tool calls.

## Example: Captured `tool_call` Event

This is what a single `tool_call` event looks like inside the `events` array of `runs/<run_id>/session.json`:

```json theme={null}
{
  "timestamp": "2025-06-01T12:00:01.234Z",
  "event_type": "tool_call",
  "source_layer": "tool_hooks",
  "tool_name": "read_file",
  "tool_call_id": "tc_abc123",
  "status": "ok",
  "duration_ms": null,
  "payload": {
    "toolName": "read_file",
    "toolCallId": "tc_abc123",
    "input": { "path": "/tmp/example.txt" }
  }
}
```

`duration_ms` is `null` at `tool_call` time because the call has not yet completed. It will be populated on the corresponding `tool_result` or `tool_execution_end` event when the `START_TIME_BY_TOOL_CALL` map is resolved.

## Related Pages

* [Architecture Overview](/architecture/overview) — The two-layer design and the full session pipeline from CLI to dashboard.
* [Observation Engine](/architecture/observation-engine) — How plugin evidence is merged with internal lifecycle events at finalize time.
