Skip to content

feat(plugins): bridge core domain events to plugin event bus#909

Merged
cryppadotta merged 2 commits intopaperclipai:masterfrom
mvanhorn:feat/plugin-domain-event-bridge
Mar 14, 2026
Merged

feat(plugins): bridge core domain events to plugin event bus#909
cryppadotta merged 2 commits intopaperclipai:masterfrom
mvanhorn:feat/plugin-domain-event-bridge

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Mar 14, 2026

Summary

The plugin event bus accepts subscriptions for core events like issue.created, and the kitchen sink example registers handlers for issue.created and issue.updated (worker.ts L891-908). But these handlers never fire because no code emits core domain events into the bus.

This adds a bridge in logActivity() so every domain action that's already logged also fires a PluginEvent to subscribing plugins.

Why this matters

The plugin system shipped today (PR #821) with a full event subscription API, but the core domain events aren't wired to it yet. This is the missing bridge.

Source Signal
Discord #dev @dotta (maintainer) "we're also adding issue-changed hooks for plugins so when that lands someone could [make notifications]"
Discord #dev @Ryze "Really excited by the plugins. I had developed a custom plugin bridge that I will now deprecate and migrate over to the new supported plugin system."
Discord #dev @choose Liberty "is there a way to have codex/claude check paperclip to see when tasks are done without me prompting it?"
Discord #dev @choose Liberty "basically to have it 'let me know when its done'"
Kitchen sink example Subscribes to issue.created and issue.updated (L891-908) but handlers never fire
Plugin SDK types.ts PLUGIN_EVENT_TYPES defines 23 event types including issue.created, issue.updated, agent.run.finished

Notification plugins, issue sync plugins, and any event-driven automation all need this bridge before they can work.

Changes

server/src/services/activity-log.ts (+30 lines)

  • Add a module-level setPluginEventBus() setter
  • After the existing publishLiveEvent() call, emit a PluginEvent when the action matches PLUGIN_EVENT_TYPES
  • Uses PLUGIN_EVENT_SET (a Set built from PLUGIN_EVENT_TYPES) to filter - non-plugin actions are not forwarded

server/src/app.ts (+2 lines)

  • Import setPluginEventBus
  • Call it after createPluginEventBus() to wire the bridge

Design decisions

  • Module-level setter follows the same pattern as the existing publishLiveEvent which uses a module-level EventEmitter. Zero call-site changes.
  • Fire-and-forget with .catch(() => {}) - plugin handler errors are already isolated by the bus.
  • Only PLUGIN_EVENT_TYPES are forwarded - random action strings don't leak through.
  • Bridge in logActivity instead of scattering eventBus.emit() across 10+ route handlers - logActivity already intercepts every domain action.

Testing

  • pnpm -r typecheck passes
  • pnpm test:run passes (73 files, 303 tests)
  • pnpm build passes

This contribution was developed with AI assistance (Claude Code).

The plugin event bus accepts subscriptions for core events like
issue.created but nothing emits them. This adds a bridge in
logActivity() so every domain action that's already logged also
fires a PluginEvent to subscribing plugins.

Uses a module-level setter (same pattern as publishLiveEvent)
to avoid threading the bus through all route handlers. Only
actions matching PLUGIN_EVENT_TYPES are forwarded.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR wires core domain events into the plugin event bus by adding a bridge inside logActivity(), mirroring the existing SSE live-events feed. The implementation is clean and follows established patterns in the codebase.

Key changes:

  • Adds a module-level setPluginEventBus() setter in activity-log.ts, following the same pattern as publishLiveEvent
  • After the existing SSE publish, constructs a PluginEvent and calls bus.emit() for any action in PLUGIN_EVENT_TYPES
  • Wires the bus in app.ts immediately after createPluginEventBus()

Two minor concerns found:

  • The emit() result (PluginEventBusEmitResult.errors) is silently discarded — plugin handler failures are invisible at the server level with no logging
  • setPluginEventBus has no guard against being called multiple times, which could silently replace the bus in test environments without any warning

Confidence Score: 4/5

  • This PR is safe to merge — the bridge logic is correct and event routing works as intended.
  • The core logic is sound: domain events are correctly constructed, filtered via PLUGIN_EVENT_SET, and dispatched to plugin subscribers. The main concerns are observability (silently discarded handler errors) and a missing double-init guard, both style-level issues that don't affect correctness.
  • No files require special attention — both changes are straightforward.

Important Files Changed

Filename Overview
server/src/services/activity-log.ts Adds a module-level plugin event bus setter and bridges domain events to plugin subscribers after the existing DB insert and SSE publish. Logic is correct; minor observability concern with silent error discarding and no double-init guard.
server/src/app.ts Imports and calls setPluginEventBus(eventBus) immediately after createPluginEventBus(). Wiring is correct and in the right order.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: server/src/services/activity-log.ts
Line: 78

Comment:
**Plugin handler errors are silently discarded**

`emit()` returns a `Promise<PluginEventBusEmitResult>` where the `errors` array holds any failures from individual plugin handlers (they're already isolated per-plugin inside the bus). By using `void ... .catch(() => {})`, the result is thrown away entirely — so if a plugin handler throws, there's no log, no metric, and no way to diagnose the failure.

The `.catch(() => {})` is also effectively dead code because the bus never rejects (it catches all handler errors internally). Consider inspecting the result and logging failures:

```suggestion
    void _pluginEventBus.emit(event).then(({ errors }) => {
      for (const { pluginId, error } of errors) {
        console.error(`[plugin-event-bridge] handler error in plugin "${pluginId}":`, error);
      }
    });
```

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

---

This is a comment left during a code review.
Path: server/src/services/activity-log.ts
Line: 16-18

Comment:
**No guard against double-initialization**

`setPluginEventBus` can be called multiple times and silently replaces the current bus with no warning. In tests where `createApp()` is called per-suite or per-test, the module-level `_pluginEventBus` will be overwritten on each call, with no indication that an existing bus has been replaced.

A warn-or-throw guard would make misuse obvious:

```suggestion
export function setPluginEventBus(bus: PluginEventBus): void {
  if (_pluginEventBus !== null && _pluginEventBus !== bus) {
    console.warn("[activity-log] setPluginEventBus called more than once — replacing existing bus");
  }
  _pluginEventBus = bus;
}
```

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

Last reviewed commit: 30e2914

runId: input.runId ?? null,
},
};
void _pluginEventBus.emit(event).catch(() => {});
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.

Plugin handler errors are silently discarded

emit() returns a Promise<PluginEventBusEmitResult> where the errors array holds any failures from individual plugin handlers (they're already isolated per-plugin inside the bus). By using void ... .catch(() => {}), the result is thrown away entirely — so if a plugin handler throws, there's no log, no metric, and no way to diagnose the failure.

The .catch(() => {}) is also effectively dead code because the bus never rejects (it catches all handler errors internally). Consider inspecting the result and logging failures:

Suggested change
void _pluginEventBus.emit(event).catch(() => {});
void _pluginEventBus.emit(event).then(({ errors }) => {
for (const { pluginId, error } of errors) {
console.error(`[plugin-event-bridge] handler error in plugin "${pluginId}":`, error);
}
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/services/activity-log.ts
Line: 78

Comment:
**Plugin handler errors are silently discarded**

`emit()` returns a `Promise<PluginEventBusEmitResult>` where the `errors` array holds any failures from individual plugin handlers (they're already isolated per-plugin inside the bus). By using `void ... .catch(() => {})`, the result is thrown away entirely — so if a plugin handler throws, there's no log, no metric, and no way to diagnose the failure.

The `.catch(() => {})` is also effectively dead code because the bus never rejects (it catches all handler errors internally). Consider inspecting the result and logging failures:

```suggestion
    void _pluginEventBus.emit(event).then(({ errors }) => {
      for (const { pluginId, error } of errors) {
        console.error(`[plugin-event-bridge] handler error in plugin "${pluginId}":`, error);
      }
    });
```

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

Comment on lines +16 to +18
export function setPluginEventBus(bus: PluginEventBus): void {
_pluginEventBus = bus;
}
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.

No guard against double-initialization

setPluginEventBus can be called multiple times and silently replaces the current bus with no warning. In tests where createApp() is called per-suite or per-test, the module-level _pluginEventBus will be overwritten on each call, with no indication that an existing bus has been replaced.

A warn-or-throw guard would make misuse obvious:

Suggested change
export function setPluginEventBus(bus: PluginEventBus): void {
_pluginEventBus = bus;
}
export function setPluginEventBus(bus: PluginEventBus): void {
if (_pluginEventBus !== null && _pluginEventBus !== bus) {
console.warn("[activity-log] setPluginEventBus called more than once — replacing existing bus");
}
_pluginEventBus = bus;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/services/activity-log.ts
Line: 16-18

Comment:
**No guard against double-initialization**

`setPluginEventBus` can be called multiple times and silently replaces the current bus with no warning. In tests where `createApp()` is called per-suite or per-test, the module-level `_pluginEventBus` will be overwritten on each call, with no indication that an existing bus has been replaced.

A warn-or-throw guard would make misuse obvious:

```suggestion
export function setPluginEventBus(bus: PluginEventBus): void {
  if (_pluginEventBus !== null && _pluginEventBus !== bus) {
    console.warn("[activity-log] setPluginEventBus called more than once — replacing existing bus");
  }
  _pluginEventBus = bus;
}
```

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

Address Greptile review feedback:
- Log plugin event handler errors via logger.warn instead of
  silently discarding the emit() result
- Warn if setPluginEventBus is called more than once

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@cryppadotta cryppadotta merged commit 2e3a0d0 into paperclipai:master Mar 14, 2026
3 checks passed
@cryppadotta
Copy link
Copy Markdown
Contributor

ty!!

@mvanhorn
Copy link
Copy Markdown
Contributor Author

Thanks for merging these, @cryppadotta!

tr00x pushed a commit to tr00x/paperclip that referenced this pull request Mar 26, 2026
…vent-bridge

feat(plugins): bridge core domain events to plugin event bus
hhhhansel pushed a commit to hhhhansel/paperclip that referenced this pull request Mar 27, 2026
…vent-bridge

feat(plugins): bridge core domain events to plugin event bus
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants