Skip to content

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

Draft
coleleavitt wants to merge 2 commits into
code-yeongyu:devfrom
coleleavitt:refactor/hook-event-filtering
Draft

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

Conversation

@coleleavitt
Copy link
Copy Markdown
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
Copy Markdown

@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.

Comment thread src/plugin/event.ts Outdated
Comment thread src/plugin/event.ts Outdated
…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())
@code-yeongyu
Copy link
Copy Markdown
Owner

[sisyphus-bot]

PR Assessment: Performance Optimization with Event Filtering

Type: Performance enhancement (not a bugfix)
Risk Level: MEDIUM (architectural change to event dispatch)
Recommendation: Needs human review - DO NOT auto-merge

Summary

This PR introduces subscription-based event filtering to reduce hot-path dispatch overhead during LLM streaming. Instead of dispatching all events to all 21 hooks sequentially, hooks now only receive events they've subscribed to.

Key Changes

  • Added HOOK_SUBSCRIPTIONS map defining which event types each hook cares about
  • Hooks without explicit subscriptions default to "*" (receive all events) for backward compatibility
  • Critical hooks (claudeCodeHooks, stopContinuationGuard, writeExistingFileGuard) are awaited; others use fire-and-forget
  • Expected reduction: ~2,100 async dispatches/sec → only subscribed hooks

Cubic AI Review Findings

Cubic raised 2 P1 concerns:

  1. Stateful hooks not receiving session.deleted - Potential resource leaks
  2. session.compacted filtered out - OpenCode compatibility break

Analysis of findings:
Upon reviewing the current code:

  • SESSION_LIFECYCLE array includes both session.deleted and session.compacted
  • compactionTodoPreserver explicitly subscribes to both events
  • stopContinuationGuard uses SESSION_LIFECYCLE which has both events

The Cubic findings may refer to an earlier commit state. The PR description mentions "fixes lifecycle subscriptions" suggesting these were addressed.

Merge Readiness Check

Condition Status
CI passing ✅ All 7 checks passed
Mergeable state ✅ MERGEABLE
Draft ✅ Not draft
Review approval NOT APPROVED
Bugfix with clear fix ❌ Performance optimization, not bugfix

Blockers to Auto-Merge

  1. No human approval - Review decision is empty
  2. Not a bugfix - This is an architectural/performance change
  3. AI reviewer flagged concerns - Cubic identified potential issues (may be resolved)

Suggested Action

Needs human maintainer review. This is a core architectural change to the event dispatch system. While CI passes and the code looks correct, the performance optimization touches critical hot-path code that affects all hook execution.

Please have a maintainer:

  1. Verify the Cubic findings are addressed in the current commit
  2. Review the subscription assignments for correctness
  3. Consider load testing to validate the performance gains

Assessment by sisyphus-bot

@code-yeongyu code-yeongyu added triage:feature PR: Feature or enhancement triage:feature-request Feature or enhancement request labels Mar 24, 2026
@code-yeongyu
Copy link
Copy Markdown
Owner

[sisyphus-bot]

PR sweep first-pass triage on dev.

  • Author: coleleavitt
  • Title: perf(hooks): add subscription-based event filtering to reduce hot-path dispatch overhead
  • Closing issues: none detected
  • Review decision: none yet
  • Mergeable state: CONFLICTING (rebase needed)
  • CI status: green / no failing checks

Needs rebase + review. Please rebase onto current dev first; then a review can land cleanly.

Assigning code-yeongyu so the maintainer can prioritize a focused review of this PR. If the linked issue above has been triaged, the verdict there is the authoritative direction for this change.

@code-yeongyu code-yeongyu self-assigned this May 16, 2026
@code-yeongyu
Copy link
Copy Markdown
Owner

[sisyphus-bot] Hi coleleavitt. 🙏 Thanks for the subscription-based event filtering perf work.

Picking this back up from the 5/16 triage. The PR is small (71/21 in 1 file) and shows CONFLICTING against current dev. The hook-dispatcher event-filtering has been touched on dev.

Could you rebase against current dev and force-push? Perf changes in the hook layer are sensitive (every event-type filter touches every hook in the 5-tier composition). After rebase, please add a brief benchmark or before/after numbers showing the reduction in hook invocations under typical workloads. Once that's in I'll loop a maintainer in for the focused review.

@coleleavitt coleleavitt marked this pull request as draft May 21, 2026 13:48
@coleleavitt
Copy link
Copy Markdown
Contributor Author

no longer contributing atm; stale PR and can be rebased if desired

@code-yeongyu code-yeongyu force-pushed the dev branch 2 times, most recently from eb25d29 to 2bfad49 Compare May 23, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:feature PR: Feature or enhancement triage:feature-request Feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants