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.
What problem are you trying to solve?
Built-in channels merge
config.eventsover 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.startedhandler both adds the status reaction and checks out the repository. The checkout populatesstate.checkoutPathbefore the model runs. A customturn.startedhandler that only posts status replaces both operations, so the turn still runs but/workspaceis empty. The same risk applies tomessage.completed, which posts the final reply, andsession.failedorturn.failed, which post error comments.The implementation is direct object replacement in
githubChannel. The coupled default behavior lives increateDefaultEvents, 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, andreplacemodes or equivalent typed helpers.Required behavior:
turn.started,message.completed,turn.failed, andsession.failed.Alternatives considered
Related: #170 documents the same replacement class in a Telegram
onMessagecustomization, but it was closed after the reporter traced the immediate failure to their replacement handler.