Skip to main content
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:
LayerAPIEvents Captured
Extension APIapi.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 HooksOpenClaw tool lifecycletool_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:
{
  "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:
{
  "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.
  • Architecture Overview — The two-layer design and the full session pipeline from CLI to dashboard.
  • Observation Engine — How plugin evidence is merged with internal lifecycle events at finalize time.