Skip to content

Make channel event handler replacement explicit and composable #623

Description

@ruiconti

What problem are you trying to solve?

Built-in channels merge config.events over their default event handlers. Supplying a handler for one event therefore replaces the channel's handler for that event instead of adding another observer.

That replacement can disable behavior unrelated to the customization. In the GitHub channel, the default turn.started handler both adds the status reaction and checks out the repository. The checkout populates state.checkoutPath before the model runs. A custom turn.started handler that only posts status replaces both operations, so the turn still runs but /workspace is empty. The same risk applies to message.completed, which posts the final reply, and session.failed or turn.failed, which post error comments.

The implementation is direct object replacement in githubChannel. The coupled default behavior lives in createDefaultEvents, and checkout-derived state starts empty until that handler runs (source).

The type documentation says handlers replace defaults, but the ordinary events: { type: handler } shape does not make the destructive choice explicit and no public helper composes with channel-configured defaults.

Proposed solution

Give built-in channel events an explicit composition contract. The exact API is open, but it should distinguish adding behavior from replacing behavior, for example with before-default, after-default, and replace modes or equivalent typed helpers.

Required behavior:

  • Adding a handler runs the built-in handler exactly once unless the author explicitly selects replacement.
  • Intentional replacement remains supported, but the call site must state that intent. A migration warning is acceptable if changing the function shorthand immediately is too disruptive.
  • Composition works with channel-specific defaults that close over configuration, including GitHub credentials and progress settings. Authors should not have to copy private default implementations.
  • Ordering and error semantics are documented. In particular, define whether a failed custom handler prevents the default from running.
  • The contract applies consistently to built-in channels and to every event with framework-owned behavior, including turn.started, message.completed, turn.failed, and session.failed.
  • Channel documentation lists the side effects owned by each default handler.
  • Tests prove that adding GitHub status behavior preserves repository checkout, final reply delivery, and failure comments. Tests should also cover explicit replacement.

Alternatives considered

  • Document replacement more prominently. This still leaves a small status customization able to disable checkout or delivery with no runtime signal.
  • Export each channel's default handler map. Configuration-bound defaults remain awkward to construct, and manual wrapping can drift as defaults change.
  • Reimplement the default in application code. That duplicates framework behavior and makes upgrades unsafe.

Related: #170 documents the same replacement class in a Telegram onMessage customization, but it was closed after the reporter traced the immediate failure to their replacement handler.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions