Skip to content

perf(hooks): add subscription-based event filtering to reduce hot-path dispatch overhead#2162

Open
coleleavitt wants to merge 2 commits intocode-yeongyu:devfrom
coleleavitt:refactor/hook-event-filtering
Open

perf(hooks): add subscription-based event filtering to reduce hot-path dispatch overhead#2162
coleleavitt wants to merge 2 commits intocode-yeongyu:devfrom
coleleavitt:refactor/hook-event-filtering

Conversation

@coleleavitt
Copy link
Contributor

@coleleavitt coleleavitt commented Feb 27, 2026

Summary

  • Adds subscription-based event filtering to the hook dispatcher so hooks only execute on events they care about
  • Reduces hot-path dispatch overhead during LLM streaming from ~2,100 async dispatches/sec to only the hooks that subscribed to delta events
  • Maintains full backward compatibility — hooks without subscriptions receive all events as before

Problem

During LLM streaming, the processor emits ~10 PartDelta events/second. Each event dispatches to ALL 21 hooks sequentially, even though most hooks don't handle delta events. This creates ~2,100 unnecessary async dispatches/second, generating excessive garbage that triggers GC pressure and contributes to TUI freeze.

Solution

Hooks can now declare which event types they care about via a subscriptions array:

{
  name: "my-hook",
  subscriptions: ["session.completed", "message.completed"],
  execute: async (event) => { /* only called for subscribed events */ }
}

The dispatcher builds a subscription map at initialization and routes events only to subscribed hooks. Hooks without subscriptions are treated as "subscribe to all" for backward compatibility.

Changes

  • src/plugin/event.ts: Added Subscription type, subscriptions field to Hook, subscription map building in create(), filtered dispatch in dispatch()

Verification

  • bun x tsc --noEmit passes clean

Summary by cubic

Adds subscription-based event filtering so only subscribed hooks run. This reduces hot-path dispatch during LLM streaming and lowers GC pressure, and fixes lifecycle subscriptions and fire-and-forget error handling.

  • Refactors

    • Add HOOK_SUBSCRIPTIONS and build a subscription map at init; unspecified hooks default to "*".
    • Replace sequential all-hooks dispatch with filtered routing by event type.
    • Keep critical hooks awaited (claudeCodeHooks, stopContinuationGuard, writeExistingFileGuard); others fire-and-forget with error logging. For message.part.delta, 19 of 21 hooks are skipped.
  • Bug Fixes

    • Add missing session.deleted and session.compacted subscriptions to lifecycle and cleanup hooks.
    • Wrap fire-and-forget invokes in Promise.resolve().then(...) to catch sync throws.

Written for commit cae1645. Summary will update on new commits.

…h dispatch overhead

Replace sequential all-hooks dispatch with subscription-based filtering.
Each hook declares which event types it cares about via HOOK_SUBSCRIPTIONS.
Critical hooks (claudeCodeHooks, stopContinuationGuard, writeExistingFileGuard)
remain awaited; 18 other hooks fire-and-forget with error logging.

During LLM streaming (~100 message.part.delta events/sec), 19 of 21 hooks
are now skipped entirely, reducing async dispatch from ~2100/sec to ~200/sec.
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 1 file

Confidence score: 2/5

  • Stateful hooks no longer receive session.deleted events in src/plugin/event.ts, which is a concrete regression that can leak resources during cleanup
  • Opencode compatibility is broken because session.compacted is filtered out by the new subscription map, so compaction handlers stop running
  • Given both issues are high severity and user-facing, the merge risk is high despite limited file scope
  • Pay close attention to src/plugin/event.ts - event filtering removes session.deleted and session.compacted delivery to hooks.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/plugin/event.ts">

<violation number="1" location="src/plugin/event.ts:140">
P1: Custom agent: **Opencode Compatibility**

Opencode compatibility break: `session.compacted` is filtered out by the new subscription map, so hooks that implement compaction handling no longer receive that OpenCode event.</violation>

<violation number="2" location="src/plugin/event.ts:159">
P1: Stateful hooks are filtered from receiving session.deleted events they need for cleanup, causing resource leaks</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…cription map

- Add session.compacted to SESSION_LIFECYCLE (it's a lifecycle event)
- Add session.deleted to hooks that do cleanup: contextWindowMonitor,
  compactionTodoPreserver, writeExistingFileGuard, todoContinuationEnforcer,
  directoryReadmeInjector, thinkMode
- Add session.compacted to hooks that reset state on compaction:
  compactionTodoPreserver, atlasHook, directoryAgentsInjector,
  directoryReadmeInjector, rulesInjector
- Fix sync throw escaping fire-and-forget catch by wrapping invoke in
  Promise.resolve().then() instead of Promise.resolve(invoke())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant