Files
video-shader-toys/docs/PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md
Aiden d4f6a4a268
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m26s
CI / Windows Release Package (push) Successful in 2m30s
phase 2 progress
2026-05-11 16:18:34 +10:00

30 KiB

Phase 2 Design: Internal Event Model

This document expands Phase 2 of ARCHITECTURE_RESILIENCE_REVIEW.md into a concrete design target.

Phase 1 established the subsystem vocabulary and moved the runtime path behind clearer collaborators. Phase 2 should now give those subsystems a safer way to coordinate than direct cross-calls, shared mutable result queues, and coarse polling loops.

Status

  • Phase 2 design package: accepted.
  • Phase 2 implementation: substantially complete for the coordination substrate.
  • Current alignment: the typed event substrate, app-owned dispatcher, coalesced app pump, reload bridge events, production bridges, and event behavior tests are in place. Remaining items are narrow follow-ups rather than foundation work.

The current repo now has concrete Phase 2 implementation footholds:

  • RuntimeEventType, typed payload structs, RuntimeEvent, RuntimeEventQueue, RuntimeEventDispatcher, and RuntimeEventCoalescingQueue define the event substrate.
  • OpenGLComposite owns one app-level RuntimeEventDispatcher and passes it into RuntimeServices, RuntimeCoordinator, RuntimeUpdateController, RuntimeSnapshotProvider, ShaderBuildQueue, and VideoBackend.
  • ControlServices publishes typed OSC and runtime-state broadcast events and uses condition-variable wakeups with a fallback compatibility timer.
  • RuntimeCoordinator publishes accepted, rejected, state-changed, persistence, reload, shader-build, and compile-status follow-up events.
  • RuntimeUpdateController subscribes to event families for broadcast, shader build, compile status, render reset, and dispatcher health observations.
  • RuntimeSnapshotProvider publishes render snapshot request/published events.
  • ShaderBuildQueue and RuntimeUpdateController publish shader build lifecycle events with generation matching.
  • VideoBackend publishes backend observation events and timing samples.
  • HealthTelemetry receives dispatcher metrics directly and the event vocabulary now includes health observation events.
  • Tests cover event type stability, payload mapping, FIFO dispatch, coalescing infrastructure, app-level coalesced broadcast/build behavior, handler failures, mutation follow-up behavior, reload bridge behavior, and shader-build generation behavior.

The implementation is now established in the repo. The remaining Phase 2 follow-up work is small: add completion/failure observations where useful and keep the runtime-store poll fallback explicitly transitional until a later file-watch implementation replaces it.

Why Phase 2 Exists

The resilience review originally called out three timing and ownership problems that an event model could directly improve:

  • background service timing relied on coarse sleeps and polling
  • control, reload, persistence, and render-update work traveled through mixed shared state and result queues
  • later render/backend refactors need a stable coordination model before they move more work across threads

The goal is not to make the app fully asynchronous in one pass. It is to introduce typed internal events so each subsystem can publish what happened without knowing who will react or how many downstream effects are needed.

Goals

Phase 2 should establish:

  • a small typed event vocabulary for control, runtime, render, backend, persistence, and health coordination
  • one app-owned event pump or dispatcher that can route events deterministically
  • bounded queues with clear ownership and no unbounded background growth
  • wakeup-driven service coordination where practical, replacing coarse polling as the default shape
  • explicit event-to-command boundaries so events do not become hidden global mutation APIs
  • tests for event ordering, coalescing, rejection, and dispatch side effects

Non-Goals

Phase 2 should not require:

  • a dedicated render thread yet
  • a full actor system
  • lock-free queues everywhere
  • background persistence implementation
  • a complete DeckLink state machine
  • final live-state layering
  • replacing every direct call in one change

Those are later phases. Phase 2 provides the coordination substrate they can build on.

Current Coordination Shape

The current runtime is much cleaner than before Phase 1, and Phase 2 has moved the main coordination model toward typed publication and app-owned dispatch:

  • ControlServices publishes OSC value, OSC commit, and runtime-state broadcast events.
  • ControlServices::PollLoop(...) is wakeup-driven for queued OSC commit work, with a bounded fallback timer for compatibility polling.
  • RuntimeCoordinator still returns RuntimeCoordinatorResult for synchronous callers, but also publishes accepted/rejected/follow-up events.
  • RuntimeUpdateController subscribes to event families and applies many effects from events rather than only from drained result objects.
  • shader-build request, readiness, failure, and application are represented by typed events.
  • render snapshot publication and backend observations are represented by typed events.
  • dispatcher queue metrics and handler failures feed telemetry and health observation events.

There is still transitional bridge-state:

  • ControlServices still exposes completed OSC commit notifications for render overlay settlement.
  • RuntimeEventCoalescingQueue is now wired into the app-owned dispatcher for latest-value event types.
  • FileChangeDetected and ManualReloadRequested are now published as reload ingress bridge events before coordinator reload follow-ups.
  • runtime-state broadcast completion/failure events are still a target, not current behavior.

That means Phase 2 is complete enough as the coordination substrate for later phases. The remaining items are refinement work and should not block moving to render ownership, live-state layering, or persistence work.

Event Model Principles

Events say what happened

Events should describe facts:

  • OscValueReceived
  • RuntimeMutationAccepted
  • RuntimeMutationRejected
  • ShaderReloadRequested
  • ShaderBuildPrepared
  • ShaderBuildFailed
  • RenderSnapshotPublished
  • RuntimeStateBroadcastRequested

They should not be vague commands like "do everything needed now."

Commands request intent

Some work is still naturally command-shaped:

  • "apply this parameter mutation"
  • "request shader reload"
  • "save this stack preset"
  • "start backend output"

Commands enter an owner subsystem. Events leave a subsystem after the owner has accepted, rejected, or completed work.

One owner mutates each state category

Events must not become a way to bypass Phase 1 ownership:

  • RuntimeCoordinator remains the owner of mutation policy.
  • RuntimeStore remains the owner of durable state.
  • RuntimeSnapshotProvider remains the owner of render snapshot publication.
  • RenderEngine remains the owner of render-local transient state.
  • VideoBackend remains the owner of device lifecycle and pacing.
  • HealthTelemetry observes and reports, but does not coordinate behavior.

Event handlers should be small

Handlers should translate events into owner calls or follow-up events. They should not accumulate hidden long-lived state unless that state belongs to the handler's subsystem.

Queues must be bounded or coalesced

High-rate control traffic can arrive faster than the app should process every individual sample. Phase 2 should preserve the useful current behavior of coalescing OSC updates by route, but make the coalescing policy explicit.

Event Families

Control Events

Produced by ControlServices.

Examples:

  • OscValueReceived
  • OscValueCoalesced
  • OscCommitRequested
  • HttpControlMutationRequested
  • WebSocketClientConnected
  • RuntimeStateBroadcastRequested
  • FileChangeDetected
  • ManualReloadRequested

Primary consumers:

  • RuntimeCoordinator
  • HealthTelemetry
  • later, a persistence writer or diagnostics publisher

Runtime Events

Produced by RuntimeCoordinator, RuntimeStore, and snapshot publication code.

Examples:

  • RuntimeMutationAccepted
  • RuntimeMutationRejected
  • RuntimeStateChanged
  • RuntimePersistenceRequested
  • RuntimeReloadRequested
  • ShaderPackagesChanged
  • RenderSnapshotPublishRequested
  • RuntimeStatePresentationChanged

Primary consumers:

  • RuntimeSnapshotProvider
  • RenderEngine
  • ControlServices
  • HealthTelemetry
  • later, PersistenceWriter

Shader Build Events

Produced by shader build orchestration and render-side build application.

Examples:

  • ShaderBuildRequested
  • ShaderBuildPrepared
  • ShaderBuildApplied
  • ShaderBuildFailed
  • CompileStatusChanged

Primary consumers:

  • RenderEngine
  • RuntimeCoordinator
  • ControlServices
  • HealthTelemetry

Render Events

Produced by RenderEngine and RuntimeSnapshotProvider.

Examples:

  • RenderSnapshotPublished
  • RenderResetRequested
  • RenderResetApplied
  • OscOverlayApplied
  • OscOverlaySettled
  • FrameRendered
  • PreviewFrameAvailable

Primary consumers:

  • RenderEngine
  • ControlServices
  • VideoBackend
  • HealthTelemetry

Backend Events

Produced by VideoBackend and backend adapters.

Examples:

  • InputSignalChanged
  • InputFrameArrived
  • OutputFrameScheduled
  • OutputFrameCompleted
  • OutputLateFrameDetected
  • OutputDroppedFrameDetected
  • BackendStateChanged

Primary consumers:

  • RenderEngine
  • HealthTelemetry
  • later, backend lifecycle state machine handlers

Health Events

Produced by all major subsystems.

Examples:

  • SubsystemWarningRaised
  • SubsystemWarningCleared
  • SubsystemRecovered
  • TimingSampleRecorded
  • QueueDepthChanged

Primary consumer:

  • HealthTelemetry

Health events should be observational. They should not be required for core behavior to proceed.

Event Envelope

A practical initial event envelope can stay simple:

enum class RuntimeEventType
{
	OscCommitRequested,
	RuntimeMutationAccepted,
	RuntimeMutationRejected,
	RuntimeReloadRequested,
	ShaderBuildRequested,
	ShaderBuildPrepared,
	ShaderBuildFailed,
	RenderSnapshotPublishRequested,
	RenderSnapshotPublished,
	RuntimeStateBroadcastRequested,
	BackendStateChanged,
	SubsystemWarningRaised
};

struct RuntimeEvent
{
	RuntimeEventType type;
	uint64_t sequence = 0;
	std::chrono::steady_clock::time_point createdAt;
	std::string source;
	std::variant<
		OscCommitRequestedEvent,
		RuntimeMutationEvent,
		ShaderBuildEvent,
		RenderSnapshotEvent,
		BackendEvent,
		HealthEvent> payload;
};

The exact C++ names can change. The key design requirements are:

  • event type is explicit
  • event order is observable
  • source subsystem is recorded
  • payload is typed, not a bag of optional strings
  • timestamps exist for queue-age telemetry
  • failures are events too, not just debug strings

Event Bus Shape

Phase 2 does not need a large framework. A small app-owned dispatcher is enough.

Suggested components:

  • RuntimeEventDispatcher
    • owns queues
    • assigns sequence numbers
    • exposes Publish(...)
    • exposes DispatchPending(...)
  • event handlers
    • narrow handler interface or function callback
    • registered by subsystem/composition root
  • RuntimeEventQueue
    • bounded FIFO for ordinary events
  • RuntimeEventCoalescingQueue
    • bounded keyed latest-value queue for flows such as high-rate OSC, broadcast requests, file/reload bursts, and queue-depth telemetry
  • queue and dispatch metrics
    • queue depth
    • oldest event age
    • dropped/coalesced counts

Initial implementation is single-process and mostly single-dispatch-thread. The important part is that event publication and event handling are explicit.

Dispatcher Ownership Decision

The first concrete implementation uses one app-owned RuntimeEventDispatcher.

Ownership:

  • OpenGLComposite owns the dispatcher as part of the current composition root.

References:

  • RuntimeServices receives the dispatcher and passes it to ControlServices.
  • RuntimeCoordinator receives the dispatcher so coordinator outcomes can become explicit events.
  • RuntimeUpdateController receives the dispatcher so it can become the first effect/apply handler.
  • RuntimeSnapshotProvider, ShaderBuildQueue, and VideoBackend receive the dispatcher so snapshot, shader lifecycle, and backend observation events are visible.

This is intentionally a composition-root dependency, not a new subsystem dependency. Subsystems should not construct their own dispatchers, and future tests should use RuntimeEventTestHarness rather than creating ad hoc event plumbing.

The dispatcher should move out of OpenGLComposite only if a later application-shell/composition-root object replaces OpenGLComposite as the owner of subsystem wiring.

Queue Policy

Not every event deserves the same queue semantics.

FIFO Events

Use FIFO for events where every item matters:

  • mutation accepted/rejected
  • shader build completed/failed
  • backend state changed
  • warning raised/cleared

Coalesced Events

Use coalescing for high-rate latest-value flows:

  • OSC parameter target updates by route
  • runtime-state broadcast requests
  • file-change reload requests during a burst
  • queue-depth telemetry

Coalesced events should record how many updates were collapsed so telemetry can show pressure.

Synchronous Boundaries

Some calls may remain synchronous during Phase 2:

  • UI/API mutation calls that need an immediate success/error response
  • startup configuration failures
  • shutdown ordering
  • tests

The rule is that synchronous calls should still publish events for accepted/rejected/completed work, so the rest of the app does not need to infer side effects from the call path.

Event Bridge Policy

This section is the implementation rulebook for converting existing direct calls and result queues into events. Future Phase 2 lanes should use this table unless they deliberately update the policy here first.

Bridge Categories

Bridge category Use when Queue shape Handler expectation
fifo-fact every occurrence matters and must be observed in order bounded FIFO handler consumes each event exactly once
coalesced-latest only the latest value per key matters bounded coalescing queue handler consumes the latest event and telemetry records collapsed count
sync-command-with-event caller needs an immediate success/error result direct owner call plus follow-up event publication handler must not be required for the caller's response
observation-only event is telemetry/diagnostic and must not drive core behavior FIFO or coalesced depending on rate handler failure must never block app behavior
compatibility-poll source cannot yet publish an event directly temporary poll adapter publishes typed events poll interval is wakeup-driven with a fallback timer until a later file-watch implementation replaces it

Current Bridge Decisions

Current flow Phase 2 bridge Event(s) Current status
OSC latest-value updates ControlServices ingress bridge OscValueReceived, optional OscValueCoalesced Event publication exists; source-side pending map and app-level dispatcher coalescing both provide latest-value behavior.
OSC commit after settle ControlServices -> RuntimeCoordinator bridge OscCommitRequested, then RuntimeMutationAccepted or RuntimeMutationRejected Event publication exists. Coordinator follow-up work now reaches the app path through events rather than a service-result queue.
HTTP/UI mutation needing response direct call into RuntimeCoordinator RuntimeMutationAccepted or RuntimeMutationRejected after the synchronous response path Implemented as sync-command-with-event; synchronous response remains supported.
runtime-state broadcast request presentation/broadcast bridge RuntimeStatePresentationChanged, RuntimeStateBroadcastRequested Request event exists, is handled, and is coalesced by the app dispatcher. Completion/failure events remain follow-ups.
manual reload button control ingress bridge ManualReloadRequested, then RuntimeReloadRequested Ingress and follow-up events exist and are covered by tests.
file watcher changes file-watch bridge FileChangeDetected, then RuntimeReloadRequested Poll fallback remains, but detected changes now publish ingress and follow-up events and are covered by tests.
runtime store poll fallback compatibility poll adapter FileChangeDetected, RuntimeReloadRequested, or warning/compile-status event Still present by design as a transitional bridge with a condition-variable fallback timer. Detected changes publish ingress and follow-up events.
shader build request runtime/render bridge ShaderBuildRequested Event publication, handler, and app dispatcher coalescing exist.
shader build ready/failure/apply shader build lifecycle bridge ShaderBuildPrepared, ShaderBuildFailed, ShaderBuildApplied, CompileStatusChanged Implemented with generation matching.
render snapshot publication snapshot bridge RenderSnapshotPublishRequested, RenderSnapshotPublished Implemented. Publish requests are coalesced by output dimensions in the app dispatcher.
render reset request/application render bridge RenderResetRequested, RenderResetApplied Request handling exists; applied event coverage can be expanded in later render work.
input signal changes backend observation bridge InputSignalChanged Implemented as backend observation publication.
output late/dropped/completed frames backend timing bridge OutputFrameCompleted, OutputLateFrameDetected, OutputDroppedFrameDetected Implemented at the vocabulary and backend publication level. High-rate policy may be refined during backend lifecycle work.
warnings and recovery telemetry bridge SubsystemWarningRaised, SubsystemWarningCleared, SubsystemRecovered Vocabulary exists; direct telemetry writes still coexist with event observations.
queue depth/timing samples telemetry metrics bridge QueueDepthChanged, TimingSampleRecorded Implemented for dispatcher/backend observations and coalesced by metric key in the app dispatcher.

Bridge Rules

  • A bridge may translate an old direct call into an owner command, but it must publish the accepted/rejected/completed event that describes the outcome.
  • A bridge must not mutate state owned by another subsystem just because it handles that subsystem's event.
  • A coalesced event must have a stable key in code and a documented policy here.
  • A FIFO event should be cheap enough that retaining every occurrence is useful. If not, turn it into a coalesced metric before putting it on a hot path.
  • A synchronous bridge must treat event publication as a side effect of the owner decision, not as the mechanism that produces the direct caller's response.
  • A compatibility poll adapter should be named as temporary in code so it does not become the new long-term coordination model.
  • Handler failure should be reported through telemetry and dispatch metrics. It should not throw back across subsystem boundaries.

First Integration Recommendation

The safest first behavior-changing bridge is RuntimeStateBroadcastRequested.

It is low risk because:

  • it is already a side effect of many coordinator outcomes
  • duplicate requests are naturally coalescable
  • the handler can call the existing ControlServices::BroadcastState() path
  • success can be verified through existing UI behavior and event tests

After that, the next bridge should be ShaderBuildRequested, because it already behaves like a queued side effect and has clear follow-up events.

Target Flow Examples

OSC Parameter Update

  1. OscServer decodes a packet.
  2. ControlServices publishes or coalesces OscValueReceived.
  3. The dispatcher routes the event to the render-overlay path or coordinator policy, depending on whether the value is transient or committing.
  4. RuntimeCoordinator publishes RuntimeMutationAccepted or RuntimeMutationRejected for committed changes.
  5. Accepted committed changes publish RenderSnapshotPublishRequested and RuntimePersistenceRequested as needed.
  6. ControlServices receives RuntimeStateBroadcastRequested or a presentation-changed event and broadcasts at its own cadence.

File Reload

  1. File-watch or manual reload produces FileChangeDetected or ManualReloadRequested.
  2. ControlServices coalesces reload bursts into one RuntimeReloadRequested.
  3. RuntimeCoordinator classifies the reload.
  4. Package/store refresh produces ShaderPackagesChanged if package metadata changed.
  5. Coordinator publishes ShaderBuildRequested.
  6. Shader build completion publishes ShaderBuildPrepared or ShaderBuildFailed.
  7. Render applies the ready build and publishes ShaderBuildApplied.

Runtime State Broadcast

  1. A mutation or reload publishes RuntimeStatePresentationChanged.
  2. ControlServices coalesces this into a broadcast request.
  3. The broadcast path asks RuntimeStatePresenter for the current presentation read model.
  4. HealthTelemetry records broadcast count, failures, and queue age.

Backend Signal Change

  1. Backend adapter detects input signal change.
  2. VideoBackend publishes InputSignalChanged.
  3. HealthTelemetry records the new signal status.
  4. Later phases may let the backend lifecycle state machine react to the same event.

Migration Plan

Step 1. Add Event Types And A Minimal Dispatcher

Status: complete.

Introduce:

  • RuntimeEvent
  • RuntimeEventType
  • typed payload structs for the smallest useful event family
  • RuntimeEventBus or equivalent dispatcher

Start with events that do not change behavior:

  • RuntimeStateBroadcastRequested
  • ShaderBuildRequested
  • RuntimeMutationRejected
  • simple health/log observations

Step 2. Convert RuntimeUpdateController Into An Event Handler

Status: complete for the Phase 2 target, with synchronous API helpers retained.

RuntimeUpdateController is already close to an event effect applier. Phase 2 should narrow it into a handler for:

  • coordinator outcome events
  • shader build readiness events
  • snapshot publication requests
  • broadcast requests

The class should stop being the place that polls every source of work.

Current note: RuntimeUpdateController now subscribes to the dispatcher and handles broadcast, reload, shader build, compile status, render reset, and health observation paths. It still accepts synchronous RuntimeCoordinatorResult values for UI/API calls that need immediate success or error responses.

Step 3. Replace ControlServices::PollLoop Sleep With Wakeups

Status: complete for OSC commit wakeups; runtime-store compatibility polling remains explicitly transitional.

Keep coalescing, but replace the fixed 25 x Sleep(10) cadence with:

  • a condition variable or waitable event
  • wakeups when OSC commit work arrives
  • wakeups when file/reload work arrives
  • a fallback timer only for compatibility polling that cannot yet be evented

This is the most direct Phase 2 timing win.

Current note: ControlServices now uses a condition variable and fallback timer. The fallback exists for runtime-store polling until a later file-watch implementation can replace scanning as the change source. Detected reload/file changes publish typed ingress and follow-up events.

Step 4. Route Shader Build Lifecycle Through Events

Status: mostly complete.

Turn the current request/apply/failure/success path into explicit events:

  • ShaderBuildRequested
  • ShaderBuildPrepared
  • ShaderBuildFailed
  • ShaderBuildApplied
  • CompileStatusChanged

This should preserve the current off-frame-path compile behavior while making readiness visible.

Current note: request, prepared, failed, applied, and compile-status events exist. Generation-aware consumption is covered by tests. Request events are coalesced by build dimensions and preserve-feedback policy in the app dispatcher.

Step 5. Route Runtime Broadcasts Through Events

Status: partially complete.

Replace direct "broadcast now" decisions with:

  • RuntimeStatePresentationChanged
  • RuntimeStateBroadcastRequested
  • RuntimeStateBroadcastCompleted
  • RuntimeStateBroadcastFailed

This keeps UI delivery in ControlServices while keeping presentation ownership in the runtime presentation layer.

Current note: RuntimeStateBroadcastRequested exists, is coalesced by the app dispatcher, and is handled. Broadcast completion/failure events have not been added yet.

Step 6. Add Event Metrics

Status: mostly complete for dispatcher metrics; broader health-event observation continues.

Before using the event system for hotter paths, add metrics:

  • event queue depth
  • oldest event age
  • event dispatch duration
  • coalesced event count
  • dropped event count
  • handler failure count

These should feed HealthTelemetry.

Current note: queue depth, oldest-event age, dispatch duration, dropped count, coalesced count, and handler failure counts feed telemetry. Queue/timing events are also published and coalesced by metric key.

Dependency Rules

Allowed:

  • producers publish events to the bus
  • the composition root registers handlers
  • handlers call owner subsystem APIs
  • HealthTelemetry observes event metrics and failures

Avoid:

  • subsystems subscribing directly to each other in constructors
  • event handlers mutating state outside their owner subsystem
  • using one global event payload with many nullable fields
  • making render hot paths block on the event bus
  • requiring health/telemetry event delivery for core behavior

The dispatcher is coordination infrastructure, not a new domain owner.

Testing Strategy

Phase 2 should add tests that do not require GL, DeckLink, or network sockets.

Implemented tests:

  • FIFO events dispatch in sequence order
  • coalesced events keep the latest payload and count collapsed updates
  • rejected mutations publish rejection events without downstream snapshot/build events
  • accepted parameter mutations publish the expected follow-up event set
  • handler failures are reported as health/log events
  • queue depth and oldest-event-age metrics update predictably
  • typed payload mapping covers persistence, render snapshot, backend, timing, queue-depth, and late/dropped output-frame events
  • shader build generation matching applies only the expected prepared build

Remaining useful tests before deeper file-watch work:

  • file reload bursts collapse into one reload request across a real poll burst
  • broadcast completion/failure events are observable once those payloads exist

The existing RuntimeEventTypeTests target is now the main pure event behavior harness. RuntimeEventTestHarness should remain the shared test helper so future lanes do not invent their own dispatcher plumbing.

Phase 2 Exit Criteria

Phase 2 can be considered complete once the project can say:

  • there is a typed internal event envelope and dispatcher
  • OpenGLComposite owns the dispatcher as the current composition root
  • ControlServices emits typed events for OSC commits and broadcast requests
  • reload/file-change work publishes typed ingress and follow-up events
  • RuntimeCoordinator publishes explicit accepted/rejected/follow-up events
  • callers no longer need broad compatibility result queues for normal runtime side effects
  • RuntimeUpdateController handles event-driven broadcast, shader build, compile status, render reset, and health observation paths
  • RuntimeUpdateController no longer needs compatibility result draining for ordinary service work
  • shader build request/readiness/failure/application is represented as events
  • shader build requests are coalesced by dimensions and preserve-feedback policy in the app path
  • render snapshot publication is represented as request/published events
  • render snapshot publish requests are coalesced in the app path where needed
  • backend observations publish typed events
  • event queues expose depth, age, dropped, coalescing, and failure metrics
  • production event paths use coalescing for broadcast requests, shader-build requests, and high-rate metrics
  • coarse sleep polling is no longer the default coordination model for OSC commit service work
  • runtime-store/file-change compatibility polling is explicitly contained and publishes event-first reload bridge events when changes are detected

Phase 2 closure note:

  • The checklist above is complete for the internal event model substrate.
  • Broadcast completion/failure events and real file-watch burst tests are useful follow-ups, but they are no longer foundation blockers.
  • RuntimeCoordinatorResult may remain as a synchronous return type for command APIs; the Phase 2 requirement is that accepted/rejected/follow-up behavior is also published as typed events, which is now true.

Open Questions For Implementation

  • Resolved: the first dispatcher is single-process, app-owned, and pumped through the current app/update path.
  • Resolved: event payloads use typed structs carried by std::variant.
  • Resolved: persistence requests are represented in Phase 2 even though background persistence lands later.
  • Resolved: backend callback events are introduced now as observation-only events.
  • Still open: should high-rate OSC transient overlay events enter the app dispatcher, or should they remain source-local until the live-state layering phase?
  • Resolved for Phase 2: RuntimeCoordinatorResult can survive as a synchronous helper for command APIs, as long as event publication remains the coordination path for downstream effects.
  • Resolved: app-level coalescing lives inside RuntimeEventDispatcher; source-specific bridges can still coalesce before publication when they own useful domain-specific collapse policy.

Short Version

Phase 2 should give the app a typed nervous system.

  • external inputs become typed events
  • owner subsystems still make decisions
  • decisions publish explicit outcomes
  • follow-up work is routed by handlers, not inferred from scattered call paths
  • high-rate work is bounded or coalesced
  • timing and queue pressure become observable

If this boundary holds, later render-thread, persistence, backend, and telemetry work can move independently without returning to shared-object polling as the default coordination model.