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 |
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 throughnormalizeEvent() 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_idfor correlation by extracting it frompayload.toolCallIdorpayload.tool_call_id. This ID ties atool_callto its eventualtool_resultortool_execution_end. - Computes
duration_msfor tool calls using aSTART_TIME_BY_TOOL_CALLmap. When atool_callortool_execution_startevent arrives, the current timestamp is stored keyed bytool_call_id. When the correspondingtool_resultortool_execution_endarrives,duration_msis computed asDate.now() - startedAtand the entry is cleared from the map. - Sets
status: "error"whenpayload.isError === true; otherwise setsstatus: "ok". - Adds a
source_layertag 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 namedmemory_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 toruns/<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:
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_msis computed from the actual wall-clock timestamps at thetool_execution_startandtool_execution_endhook 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:
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 — 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.