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:
Doesn't scale (one Go shim per plugin capability)
Violates plugin architecture (plugins should define capabilities)
Duplicates logic (handler behavior split across Go and plugin)
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:
Initialize returns InitializeResponse with handler_names[]
Rejected: More complex, harder to debug, less direct control flow
Related Decisions
ADR-001: Established plugin architecture foundation
Phase 5 (Future): Migrate ixgest.git to code plugin for pure plugin-based system
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, budget management).
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.