Skip to content

fix(delivery): deliverOutboundPayloads return type cannot distinguish hook cancellations from failures #57766

@Kaspre

Description

@Kaspre

Problem

`deliverOutboundPayloads()` returns `OutboundDeliveryResult[]`, but an empty array (`[]`) is ambiguous — it can mean:

  1. Hook cancellation: A `message_sending` hook cancelled all payloads via `{ cancel: true }`. This is intentional policy, not a failure.
  2. Delivery failure: The send was attempted but produced zero results (e.g. channel plugin returned nothing).

Callers have no way to distinguish these cases. This matters for #53961 and #57755, which add delivery status tracking — both currently report hook cancellations as `deliveryStatus.succeeded: false`, which is incorrect.

Second issue: partial delivery on non-bestEffort throw

When `bestEffortDeliver` is false and delivery throws mid-stream, `deliverOutboundPayloadsCore()` may have already sent earlier payloads (the results array is built incrementally). The thrown error carries no information about what was already delivered, so callers report `succeeded: false` even though some messages reached the channel. Retrying based on that status can duplicate the already-delivered prefix.

Proposed fix

Enrich the return type to carry outcome metadata:

```typescript
interface DeliveryOutcome {
results: OutboundDeliveryResult[];
/** True when all payloads were cancelled by message_sending hooks. /
cancelledByHook: boolean;
/
* Number of payloads that were successfully sent before a throw (non-bestEffort). */
sentBeforeError?: number;
}
```

This would let callers distinguish:

  • `results.length === 0 && cancelledByHook` → policy suppression, not failure
  • `sentBeforeError > 0` on catch → partial delivery, don't retry blindly

Impact

Scope

Changes are in `src/infra/outbound/deliver.ts` (`deliverOutboundPayloadsCore` and the outer wrapper). All callers of `deliverOutboundPayloads` would need to handle the new return shape, but the change is backward-compatible if structured as `results & { cancelledByHook?: boolean }`.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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