Skip to content

feat: agent event hooks + streaming for channel plugins#48355

Closed
carlos-sarmiento wants to merge 1 commit into
openclaw:mainfrom
carlos-sarmiento:feat/agent-event-hooks
Closed

feat: agent event hooks + streaming for channel plugins#48355
carlos-sarmiento wants to merge 1 commit into
openclaw:mainfrom
carlos-sarmiento:feat/agent-event-hooks

Conversation

@carlos-sarmiento

Copy link
Copy Markdown

Summary

Exposes agent cognitive lifecycle events to channel plugins, enabling rich agentic UIs (thinking blocks, tool call cards, usage tracking).

Changes

Commit 1: thinking hooks + enriched agent_end (5 files, +117)

  • New thinking_start / thinking_end plugin hooks fired during reasoning phases
  • agent_end event now includes tokenUsage (input/output/cache/total) and toolCallCount
  • Added "thinking" to AgentEventStream type union

Commit 2: api.onAgentEvent() for real-time streaming (2 files, +12)

  • Exposes the internal agent event bus via api.onAgentEvent(listener) on OpenClawPluginApi
  • Returns unsubscribe function for cleanup
  • Receives all events: thinking deltas, tool start/result, assistant text, lifecycle

Motivation

Channel plugins currently have before_tool_call / after_tool_call hooks but no visibility into:

  • Thinking/reasoning phases (no hooks existed)
  • Token usage per run (not exposed in agent_end)
  • Real-time streaming of thinking text deltas

This unlocks channel plugins that render:

  • Collapsible thinking blocks with streamed text
  • Tool call progress cards
  • Per-message cost/token badges
  • Rich typing indicators ("Thinking...", "Running exec...")

Usage

// Lifecycle hooks (discrete moments)
api.on("thinking_start", (evt, ctx) => { /* show indicator */ });
api.on("thinking_end", (evt, ctx) => { /* render block with evt.text */ });
api.on("agent_end", (evt, ctx) => {
  // evt.tokenUsage.total, evt.toolCallCount
});

// Real-time streaming (high-frequency deltas)
const unsub = api.onAgentEvent((evt) => {
  if (evt.stream === "thinking") {
    // evt.data.delta — incremental thinking text
  }
  if (evt.stream === "tool" && evt.data?.phase === "start") {
    // evt.data.name — tool being called
  }
});

Files Changed

  • src/plugins/types.ts — New hook types, enriched AgentEndEvent, onAgentEvent on API
  • src/plugins/hooks.tsrunThinkingStart() / runThinkingEnd() implementations
  • src/plugins/registry.ts — Wire onAgentEvent into plugin API creation
  • src/agents/pi-embedded-subscribe.handlers.messages.ts — Emit thinking hooks
  • src/agents/pi-embedded-runner/run/attempt.ts — Thread usage/tool count into agent_end
  • src/infra/agent-events.ts — Add "thinking" to stream type

Closes #7724
Related: #5086, #5279, #21184, #30285

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 16, 2026
@greptile-apps

greptile-apps Bot commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds agent cognitive lifecycle hooks (thinking_start, thinking_end) and a real-time event streaming API (api.onAgentEvent()) to channel plugins, enriching agent_end with token usage and tool call counts. The motivation is sound and the overall architecture fits naturally into the existing hook runner and event bus patterns. However, there are three logic issues that need attention before merge.

  • thinking_end hook can fire multiple times — Unlike thinking_start (guarded by !reasoningStreamOpen), thinking_end has no deduplication guard. When providers send duplicate thinking_end events, the existing code forces reasoningStreamOpen = true before emitReasoningEnd, causing the hook to fire on each duplicate.
  • thinking_end fires without a matching thinking_start in edge cases — If a provider skips thinking_start/thinking_delta and delivers thinking_end directly, the thinking_start hook is never fired but thinking_end is. Plugin authors building stateful UIs (open-on-start / close-on-end) will encounter unmatched events.
  • PluginHookThinkingEndEvent.durationMs is declared but never set — No start timestamp is tracked anywhere in the implementation, so this public API field will always be undefined, silently breaking any plugin that renders "Thought for X seconds".
  • onAgentEvent is globally scoped — Listeners receive events from all concurrent runs and all sessions with no automatic filtering. This is an undocumented information-disclosure risk in multi-user deployments and should be documented (or scoped) clearly.

Confidence Score: 2/5

  • Not safe to merge — multiple logic bugs in the new hook emission path will cause plugin state inconsistencies and expose a misleading public API field.
  • Three concrete logic bugs: duplicate thinking_end hook firings, mismatched thinking_end without thinking_start in edge cases, and a durationMs field that is always undefined. Additionally, onAgentEvent is globally scoped with no session filtering, which is a latent information-disclosure issue. The token-usage enrichment in agent_end and the hooks.ts implementation are clean and have no issues.
  • src/agents/pi-embedded-subscribe.handlers.messages.ts needs the most attention for the hook deduplication and mismatched-pair issues. src/plugins/types.ts needs durationMs either implemented or removed. src/plugins/registry.ts needs the global-scope concern addressed or documented.

Comments Outside Diff (1)

  1. src/agents/pi-embedded-subscribe.handlers.messages.ts, line 134-153 (link)

    thinking_end hook can fire multiple times

    The thinking_start hook is correctly guarded by if (!ctx.state.reasoningStreamOpen) so it fires exactly once. The thinking_end hook has no equivalent guard.

    When thinking_end arrives, lines 135–136 forcefully set reasoningStreamOpen = true (to handle providers that skip thinking_start/thinking_delta), then emitReasoningEnd resets it to false. If a second thinking_end event arrives (which some providers do send), the same path repeats — reasoningStreamOpen is forced back to true, emitReasoningEnd fires again, and the hook fires a second time.

    Plugin authors subscribing to thinking_end will be surprised to receive it more than once per thinking block, especially since thinking_start is guaranteed to fire only once. Consider adding a guard analogous to the one used for thinking_start, or track a separate boolean (e.g., thinkingEndHookFired) that is reset at the same boundary as reasoningStreamOpen:

    if (evtType === "thinking_end") {
      if (!ctx.state.reasoningStreamOpen) {
        ctx.state.reasoningStreamOpen = true;
      }
      emitReasoningEnd(ctx);
      // Emit thinking_end hook — only if the stream was actually open (i.e. we had a thinking phase)
      if (!ctx.state.reasoningEndHookFired) {
        ctx.state.reasoningEndHookFired = true;
        const hookRunner = ctx.hookRunner ?? getGlobalHookRunner();
        if (hookRunner?.hasHooks("thinking_end")) {
          // ...
        }
      }
    }

    (Reset reasoningEndHookFired at the same place reasoningStreamOpen is reset — emitReasoningEnd or resetAssistantMessageState.)

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/pi-embedded-subscribe.handlers.messages.ts
    Line: 134-153
    
    Comment:
    **`thinking_end` hook can fire multiple times**
    
    The `thinking_start` hook is correctly guarded by `if (!ctx.state.reasoningStreamOpen)` so it fires exactly once. The `thinking_end` hook has no equivalent guard.
    
    When `thinking_end` arrives, lines 135–136 _forcefully_ set `reasoningStreamOpen = true` (to handle providers that skip `thinking_start`/`thinking_delta`), then `emitReasoningEnd` resets it to `false`. If a second `thinking_end` event arrives (which some providers do send), the same path repeats — `reasoningStreamOpen` is forced back to `true`, `emitReasoningEnd` fires again, and the hook fires a second time.
    
    Plugin authors subscribing to `thinking_end` will be surprised to receive it more than once per thinking block, especially since `thinking_start` is guaranteed to fire only once. Consider adding a guard analogous to the one used for `thinking_start`, or track a separate boolean (e.g., `thinkingEndHookFired`) that is reset at the same boundary as `reasoningStreamOpen`:
    
    ```typescript
    if (evtType === "thinking_end") {
      if (!ctx.state.reasoningStreamOpen) {
        ctx.state.reasoningStreamOpen = true;
      }
      emitReasoningEnd(ctx);
      // Emit thinking_end hook — only if the stream was actually open (i.e. we had a thinking phase)
      if (!ctx.state.reasoningEndHookFired) {
        ctx.state.reasoningEndHookFired = true;
        const hookRunner = ctx.hookRunner ?? getGlobalHookRunner();
        if (hookRunner?.hasHooks("thinking_end")) {
          // ...
        }
      }
    }
    ```
    
    (Reset `reasoningEndHookFired` at the same place `reasoningStreamOpen` is reset — `emitReasoningEnd` or `resetAssistantMessageState`.)
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-subscribe.handlers.messages.ts
Line: 134-153

Comment:
**`thinking_end` hook can fire multiple times**

The `thinking_start` hook is correctly guarded by `if (!ctx.state.reasoningStreamOpen)` so it fires exactly once. The `thinking_end` hook has no equivalent guard.

When `thinking_end` arrives, lines 135–136 _forcefully_ set `reasoningStreamOpen = true` (to handle providers that skip `thinking_start`/`thinking_delta`), then `emitReasoningEnd` resets it to `false`. If a second `thinking_end` event arrives (which some providers do send), the same path repeats — `reasoningStreamOpen` is forced back to `true`, `emitReasoningEnd` fires again, and the hook fires a second time.

Plugin authors subscribing to `thinking_end` will be surprised to receive it more than once per thinking block, especially since `thinking_start` is guaranteed to fire only once. Consider adding a guard analogous to the one used for `thinking_start`, or track a separate boolean (e.g., `thinkingEndHookFired`) that is reset at the same boundary as `reasoningStreamOpen`:

```typescript
if (evtType === "thinking_end") {
  if (!ctx.state.reasoningStreamOpen) {
    ctx.state.reasoningStreamOpen = true;
  }
  emitReasoningEnd(ctx);
  // Emit thinking_end hook — only if the stream was actually open (i.e. we had a thinking phase)
  if (!ctx.state.reasoningEndHookFired) {
    ctx.state.reasoningEndHookFired = true;
    const hookRunner = ctx.hookRunner ?? getGlobalHookRunner();
    if (hookRunner?.hasHooks("thinking_end")) {
      // ...
    }
  }
}
```

(Reset `reasoningEndHookFired` at the same place `reasoningStreamOpen` is reset — `emitReasoningEnd` or `resetAssistantMessageState`.)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/plugins/types.ts
Line: 1787-1788

Comment:
**`durationMs` declared but never populated**

`PluginHookThinkingEndEvent` exposes `durationMs?: number`, but nothing in the implementation tracks the start timestamp of the thinking phase. The `runThinkingEnd` call in `pi-embedded-subscribe.handlers.messages.ts` only passes `runId` and `text``durationMs` is always `undefined`.

Plugin authors relying on this field for performance measurements (e.g., displaying "Thought for 3.2s") will silently receive `undefined`.

Either track the start time when `thinking_start` fires:

```typescript
// In the thinking_start branch:
ctx.state.thinkingStartedAt = Date.now();

// In the thinking_end branch:
durationMs: ctx.state.thinkingStartedAt ? Date.now() - ctx.state.thinkingStartedAt : undefined,
```

Or remove `durationMs` from the type until it is actually implemented, to avoid misleading the public API.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/plugins/registry.ts
Line: 852

Comment:
**`onAgentEvent` exposes cross-session events to every plugin**

`onAgentEvent` registers a listener on the **global** module-level `listeners` set in `agent-events.ts`. Every plugin that calls `api.onAgentEvent(listener)` will receive events from _all_ concurrent agent runs and all sessions — there is no automatic filtering by `sessionKey`, `agentId`, or tenant.

In a multi-user or multi-session deployment, Plugin A belonging to User 1 will receive thinking deltas, tool invocations, and assistant text originating from User 2's agent run. This is an information disclosure risk.

The JSDoc on `OpenClawPluginApi.onAgentEvent` in `types.ts` does not mention this global scope, so plugin authors will likely assume they only receive events for their own context.

Consider either:
1. Filtering by `sessionKey` automatically inside the wrapper — e.g. capture the `sessionKey` at registration time and skip unrelated events.
2. Documenting prominently that the listener is global and that plugin authors must filter by `evt.sessionKey` themselves.
3. Scoping subscription to a specific `runId` or `agentId` by accepting an optional filter parameter.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/pi-embedded-subscribe.handlers.messages.ts
Line: 139-153

Comment:
**`thinking_end` hook can fire without a matching `thinking_start`**

When `thinking_end` is the very first event (some providers skip `thinking_start`/`thinking_delta`), the `thinking_start` block (lines 101–115) never runs because its outer condition is `evtType === "thinking_start" || evtType === "thinking_delta"`. The `thinking_start` hook is therefore never fired.

However, the `thinking_end` hook _does_ fire unconditionally for any `thinking_end` event. This means plugin authors writing stateful UI (e.g., open a collapsible block on `thinking_start`, close it on `thinking_end`) will encounter an unmatched `thinking_end` and likely a crash or inconsistent render.

Consider emitting a synthetic `thinking_start` hook before the `thinking_end` hook in this edge-case path, or documenting that `thinking_end` can arrive without a prior `thinking_start`.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: a1e0854

Comment thread src/plugins/types.ts
Comment thread src/plugins/registry.ts Outdated
Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1e0854637

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/plugins/registry.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b3901c8119

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts
Comment thread src/agents/pi-embedded-runner/run/attempt.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a286ab506a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5821718b68

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts Outdated
Comment thread src/agents/pi-embedded-runner/run/attempt.ts Outdated
@oribot-o
oribot-o force-pushed the feat/agent-event-hooks branch 2 times, most recently from 10f7436 to de52bf0 Compare March 21, 2026 17:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de52bf0e7c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/plugins/registry.ts Outdated
Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts Outdated
@oribot-o
oribot-o force-pushed the feat/agent-event-hooks branch from de52bf0 to 7a3e2ca Compare March 21, 2026 17:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a3e2cabd5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts
Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts Outdated
Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts Outdated
@oribot-o
oribot-o force-pushed the feat/agent-event-hooks branch from 7a3e2ca to 15b5017 Compare March 22, 2026 21:35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15b5017bd5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts
Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts Outdated
@oribot-o
oribot-o force-pushed the feat/agent-event-hooks branch from 15b5017 to e1559d8 Compare March 23, 2026 15:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1559d8239

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts Outdated
Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts
Comment thread src/plugins/registry.ts Outdated
@openclaw-barnacle openclaw-barnacle Bot added the extensions: lobster Extension: lobster label Mar 23, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ca6245452

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/plugins/registry.ts Outdated
Comment on lines +1014 to +1015
const ctx = getAgentRunContext(evt.runId);
if (ctx?.sessionKey === sk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve session filter before dispatching listeners

api.onAgentEvent falls back to getAgentRunContext(evt.runId) when evt.sessionKey is missing, but that lookup happens inside each listener callback. On non-Control-UI runs, lifecycle end/error events have no sessionKey, and the gateway listener clears run context during those same events (clearAgentRunContext). Because listeners are invoked in registration order, deferred channel plugins (loaded later in server.impl.ts full-load path) can run after the clear and miss the terminal lifecycle event for their session, leaving per-run plugin state open even though the run ended.

Useful? React with 👍 / 👎.

@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Mar 23, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35f0e249d8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/pi-embedded-subscribe.handlers.messages.ts
- Add thinking_start/thinking_end plugin hooks with durationMs tracking
- Add token usage (input/output/cache) and toolCallCount to agent_end hook
- Expose onAgentEvent() in plugin API with optional sessionKey filtering
- Gate onAgentEvent behind registrationMode='full' to prevent listener leaks
- Emit synthetic thinking_start before orphaned thinking_end events
- Track toolCallCount independently of compaction retry resets
- Fire thinking hooks for <think> tag reasoning flows (not just native events)
- Hoist tag transition tracking outside if(next) gate for pure-thinking chunks

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22b603402b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +464 to +465
extractAssistantThinking(msg) ||
extractThinkingFromTaggedText(extractAssistantTextWithThinkingTags(msg));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore code spans when extracting fallback tagged reasoning

The message_end safety net now derives fallback thinking text with extractThinkingFromTaggedText(extractAssistantTextWithThinkingTags(msg)), but that extractor is regex-only and does not skip inline/code-fence spans. If a run has an open reasoning lifecycle (for example from native thinking_* events) and the assistant answer contains a literal code example like `<think>...</think>`, the thinking_end hook can receive bogus reasoning text from user-visible code instead of real model reasoning, causing channel plugins to render incorrect thinking blocks.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The PR adds plugin-facing thinking lifecycle hooks, a top-level api.onAgentEvent stream subscription, token/tool metadata on agent_end, event filtering tests, and generated SDK baseline updates.

Reproducibility: yes. for the review findings: source inspection of current main and PR head reproduces the API-shape mismatch, duplicate native thinking_end path, and regex-only fallback extraction. I did not run tests because this review is read-only.

Real behavior proof
Needs real behavior proof before merge: Missing: the contributor should add redacted terminal/log/screenshot/video proof from a real plugin/channel setup, update the PR body, and ask for @clawsweeper re-review if needed.

Next step before merge
Needs contributor real-behavior proof plus maintainer API-shape/rebase review; automation should not repair or merge while the proof gate is missing.

Security
Cleared: No dependency, workflow, secret-handling, permission, or supply-chain regression was found in the PR surface, but the event API should keep the explicit session-scoping contract.

Review findings

  • [P2] Wire the event API through current builders — src/plugins/types.ts:1392-1395
  • [P2] Ignore duplicate native thinking_end events — src/agents/pi-embedded-subscribe.handlers.messages.ts:223-241
  • [P2] Use code-span-aware fallback extraction — src/agents/pi-embedded-subscribe.handlers.messages.ts:463-465
Review details

Best possible solution:

Rebase onto current main, use the host-owned grouped plugin event subscription contract unless maintainers approve a new top-level API, make thinking lifecycle delivery deterministic, and provide redacted real plugin/channel proof before merge.

Do we have a high-confidence way to reproduce the issue?

Yes for the review findings: source inspection of current main and PR head reproduces the API-shape mismatch, duplicate native thinking_end path, and regex-only fallback extraction. I did not run tests because this review is read-only.

Is this the best way to solve the issue?

No, not as-is. The direction is plausible, but the maintainable solution should align with current main's grouped plugin event contract and guarantee one-to-one thinking lifecycle edges before exposing the SDK surface.

Full review comments:

  • [P2] Wire the event API through current builders — src/plugins/types.ts:1392-1395
    Adding a required top-level OpenClawPluginApi.onAgentEvent member conflicts with current main's api.agent.events.registerAgentEventSubscription(...) facade and API builder. Rebase this onto the grouped event contract or update every current builder/helper that constructs OpenClawPluginApi, or SDK/helper consumers will see a required member that the host does not consistently provide.
    Confidence: 0.86
  • [P2] Ignore duplicate native thinking_end events — src/agents/pi-embedded-subscribe.handlers.messages.ts:223-241
    After a normal native thinking block closes, a repeated provider thinking_end can enter this synthetic-start path and emit another thinking lifecycle pair for the same reasoning block. Track the closed native block or ignore duplicate terminal events so plugin UIs do not double-count or flicker.
    Confidence: 0.86
  • [P2] Use code-span-aware fallback extraction — src/agents/pi-embedded-subscribe.handlers.messages.ts:463-465
    The message_end fallback extracts tagged thinking with a regex-only helper. If native thinking is open and the final answer contains a literal <think>...</think> example in code, plugins can receive fake reasoning text; reuse the code-span-aware parser or avoid parsing user-visible code in this fallback.
    Confidence: 0.78

Overall correctness: patch is incorrect
Overall confidence: 0.86

What I checked:

  • Live PR state: The PR is open, external, non-draft, authored by Carlos Gustavo Sarmiento (@carlos-sarmiento, account created 2012-06-28); latest head is 22b6034 and CI still has two failing pnpm test:channels shards. (22b603402ba1)
  • Current main plugin event contract: Current main exposes plugin event subscriptions as api.agent.events.registerAgentEventSubscription(...) and emitAgentEvent(...), not a new top-level api.onAgentEvent(...) API. (src/plugins/types.ts:2502, 936989a88b1d)
  • Current main API builder wiring: buildPluginApi(...) wires registerAgentEventSubscription and emitAgentEvent, then attaches grouped facades; it has no top-level onAgentEvent member. (src/plugins/api-builder.ts:256, 936989a88b1d)
  • Current docs prefer grouped SDK namespaces: The plugin SDK overview documents api.agent.events.registerAgentEventSubscription(...) as the grouped namespace for sanitized event subscriptions and says new plugin code should use grouped namespaces. Public docs: docs/plugins/sdk-overview.md. (docs/plugins/sdk-overview.md:152, 936989a88b1d)
  • Current main partially overlaps but does not cover the PR: Current main already emits stream: "thinking" agent events from emitReasoningStream, but only when state.streamReasoning and params.onReasoningStream are present, so it does not fully replace the PR's plugin-facing event API and hook additions. (src/agents/pi-embedded-subscribe.ts:878, 936989a88b1d)
  • Current main lacks thinking hook names: The current PluginHookName union does not include thinking_start or thinking_end, so the PR still has unique SDK surface beyond main. (src/plugins/hook-types.ts:68, 936989a88b1d)

Likely related people:

  • 100yenadmin: Authored the generic plugin host-hook contracts commit that introduced the current registerAgentEventSubscription path this PR must align with. (role: introduced behavior; confidence: high; commits: 1adaa28dc86b; files: src/plugins/types.ts, src/plugins/api-builder.ts, src/plugins/registry.ts)
  • jalehman: Listed as reviewer and co-author on the generic plugin host-hook contracts commit that owns the current plugin event subscription contract. (role: reviewer; confidence: medium; commits: 1adaa28dc86b; files: src/plugins/types.ts, src/plugins/host-hook-runtime.ts, src/plugins/registry.ts)
  • medns: Recently refactored raw reasoning streaming and touched the subscribe/message utility path central to thinking-event behavior. (role: recent area contributor; confidence: medium; commits: e7277b4e3a4b; files: src/agents/pi-embedded-subscribe.handlers.messages.ts, src/agents/pi-embedded-subscribe.ts, src/agents/pi-embedded-utils.ts)
  • cathrynlavery: Recently changed src/infra/agent-events.ts session-key preservation for hidden lifecycle events, directly adjacent to the session-scoping concern for plugin event subscriptions. (role: recent area contributor; confidence: medium; commits: 763a88083e63; files: src/infra/agent-events.ts, src/infra/agent-events.test.ts)

Remaining risk / open question:

  • The external PR lacks after-fix real behavior proof from an actual plugin/channel setup.
  • The proposed public SDK shape needs maintainer API judgment because current main now documents grouped api.agent.events.* namespaces.
  • Latest PR-head CI still has failing channel test shards, and this read-only review did not rerun tests locally.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 936989a88b1d.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docs Improvements or additions to documentation extensions: lobster Extension: lobster size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Agent loop lifecycle hooks (thinking, response, message events)

1 participant