ADR-004: Plugin-Pulse Integration for Dynamic Async Handlers

Status: Accepted Date: 2026-01-31 Deciders: QNTX Core Team Related: ADR-001 (Domain Plugin Architecture)

Context

Pulse (async job system) and the Plugin system existed as parallel systems. Plugins could enqueue jobs but not execute them. Adding plugin-based async capabilities required writing Go handler shims that manually called plugins via gRPC, violating the "minimal core" philosophy and plugin architecture.

Problem

Current: Plugin wants async capability
  → Write Go handler in pulse/async/
  → Go handler calls plugin via gRPC
  → Register Go handler manually
  → Domain logic leaks into core

This pattern:

Decision

Enable bidirectional integration: plugins register async handlers directly with Pulse, eliminating Go shims.

Architecture

New: Plugin announces async handlers
  → Server registers handlers with Pulse
  → Pulse routes jobs to plugins via gRPC
  → Plugin executes and returns results
  → No Go domain logic required

Protocol Changes

Extended domain.proto (Plugin gRPC API):

Handler Registration Flow

  1. Plugin startup: Plugin loads, announces capabilities
  2. After initialization: Server registers announced handlers with Pulse
  3. Job execution: Pulse routes to plugin via PluginProxyHandler
  4. Results: Plugin returns progress/cost/result to Pulse

Implementation Phases

Phase 1: Protocol Foundation ✅

Phase 2: Plugin Execution ✅

Phase 3: Dynamic Handler Discovery ✅

Phase 4: Registration Timing ✅

Phase 5: Reactive Watchers (PR #770) ✅

Consequences

Positive

Extensibility: New plugin = new async capabilities automatically

Clean Architecture:

Dynamic Capabilities:

Reduced Core Complexity:

Negative

gRPC Overhead:

Plugin Crashes:

Version Compatibility:

Alternatives Considered

1. Keep Go Handler Shims

Rejected: Violates plugin architecture, doesn't scale, duplicates logic

2. Plugin SDK for Direct Queue Access

Rejected: Bypasses Pulse routing, loses centralized job management, no progress tracking

3. Event-Based Integration

Rejected: More complex, harder to debug, less direct control flow

Related Decisions

Notes

This decision removes the last barrier to a fully plugin-based async execution system. Domain logic (Python execution, git ingestion, etc.) now lives entirely in plugins, with core QNTX providing only generic infrastructure (routing, queuing, progress tracking).

The self-certifying attestation pattern for handlers enables unlimited user-created handlers without hitting bounded storage limits, as each handler acts as its own actor.