From 30e2914424343e2673ba084b1740dd5781c2dd74 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:44:26 -0700 Subject: [PATCH 1/2] feat(plugins): bridge core domain events to plugin event bus 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) --- server/src/app.ts | 2 ++ server/src/services/activity-log.ts | 32 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/server/src/app.ts b/server/src/app.ts index 22183848..c2034eba 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -37,6 +37,7 @@ import { pluginLifecycleManager } from "./services/plugin-lifecycle.js"; import { createPluginJobCoordinator } from "./services/plugin-job-coordinator.js"; import { buildHostServices, flushPluginLogBuffer } from "./services/plugin-host-services.js"; import { createPluginEventBus } from "./services/plugin-event-bus.js"; +import { setPluginEventBus } from "./services/activity-log.js"; import { createPluginDevWatcher } from "./services/plugin-dev-watcher.js"; import { createPluginHostServiceCleanup } from "./services/plugin-host-service-cleanup.js"; import { pluginRegistryService } from "./services/plugin-registry.js"; @@ -141,6 +142,7 @@ export async function createApp( const workerManager = createPluginWorkerManager(); const pluginRegistry = pluginRegistryService(db); const eventBus = createPluginEventBus(); + setPluginEventBus(eventBus); const jobStore = pluginJobStore(db); const lifecycle = pluginLifecycleManager(db, { workerManager }); const scheduler = createPluginJobScheduler({ diff --git a/server/src/services/activity-log.ts b/server/src/services/activity-log.ts index cdef68ec..4c7248bd 100644 --- a/server/src/services/activity-log.ts +++ b/server/src/services/activity-log.ts @@ -1,8 +1,21 @@ +import { randomUUID } from "node:crypto"; import type { Db } from "@paperclipai/db"; import { activityLog } from "@paperclipai/db"; +import { PLUGIN_EVENT_TYPES, type PluginEventType } from "@paperclipai/shared"; +import type { PluginEvent } from "@paperclipai/plugin-sdk"; import { publishLiveEvent } from "./live-events.js"; import { redactCurrentUserValue } from "../log-redaction.js"; import { sanitizeRecord } from "../redaction.js"; +import type { PluginEventBus } from "./plugin-event-bus.js"; + +const PLUGIN_EVENT_SET: ReadonlySet = new Set(PLUGIN_EVENT_TYPES); + +let _pluginEventBus: PluginEventBus | null = null; + +/** Wire the plugin event bus so domain events are forwarded to plugins. */ +export function setPluginEventBus(bus: PluginEventBus): void { + _pluginEventBus = bus; +} export interface LogActivityInput { companyId: string; @@ -45,4 +58,23 @@ export async function logActivity(db: Db, input: LogActivityInput) { details: redactedDetails, }, }); + + if (_pluginEventBus && PLUGIN_EVENT_SET.has(input.action)) { + const event: PluginEvent = { + eventId: randomUUID(), + eventType: input.action as PluginEventType, + occurredAt: new Date().toISOString(), + actorId: input.actorId, + actorType: input.actorType, + entityId: input.entityId, + entityType: input.entityType, + companyId: input.companyId, + payload: { + ...redactedDetails, + agentId: input.agentId ?? null, + runId: input.runId ?? null, + }, + }; + void _pluginEventBus.emit(event).catch(() => {}); + } } From a6c7e09e2a01ef1520967d9654e7fe3f9e14b008 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:51:41 -0700 Subject: [PATCH 2/2] fix(plugins): log plugin handler errors, warn on double-init 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) --- server/src/services/activity-log.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/services/activity-log.ts b/server/src/services/activity-log.ts index 4c7248bd..16758b94 100644 --- a/server/src/services/activity-log.ts +++ b/server/src/services/activity-log.ts @@ -6,6 +6,7 @@ import type { PluginEvent } from "@paperclipai/plugin-sdk"; import { publishLiveEvent } from "./live-events.js"; import { redactCurrentUserValue } from "../log-redaction.js"; import { sanitizeRecord } from "../redaction.js"; +import { logger } from "../middleware/logger.js"; import type { PluginEventBus } from "./plugin-event-bus.js"; const PLUGIN_EVENT_SET: ReadonlySet = new Set(PLUGIN_EVENT_TYPES); @@ -14,6 +15,9 @@ let _pluginEventBus: PluginEventBus | null = null; /** Wire the plugin event bus so domain events are forwarded to plugins. */ export function setPluginEventBus(bus: PluginEventBus): void { + if (_pluginEventBus) { + logger.warn("setPluginEventBus called more than once, replacing existing bus"); + } _pluginEventBus = bus; } @@ -75,6 +79,10 @@ export async function logActivity(db: Db, input: LogActivityInput) { runId: input.runId ?? null, }, }; - void _pluginEventBus.emit(event).catch(() => {}); + void _pluginEventBus.emit(event).then(({ errors }) => { + for (const { pluginId, error } of errors) { + logger.warn({ pluginId, eventType: event.eventType, err: error }, "plugin event handler failed"); + } + }).catch(() => {}); } }