# Service Event Delivery

> Canonical path: `specs/service/service-event-delivery.md`
> Status: living-spec
> Scope: service-owned event subscriptions, webhook receipts, provider delivery ledgers, App interaction delivery ledgers, and service-authored delivery effects.
>
> Related specs:
> - [`specs/architecture/service-actor-event-automation.md`](../architecture/service-actor-event-automation.md)
> - [`specs/service/service-actor-platform.md`](./service-actor-platform.md)
> - [`specs/app/app-notification.md`](../app/app-notification.md)
> - [`specs/automation/run-effect-contract.md`](../automation/run-effect-contract.md)
> - [`specs/collaboration/source-projection-delivery.md`](../collaboration/source-projection-delivery.md)
>
## 1. Purpose

Service Event Delivery records events owned by service actors and the effects of
those events in AgentTeam. The current event family is GitHub/Gitea pull request
subscription delivery. The model is intentionally generic so future service or
App events can use the same substrate.

## 2. Tables and responsibilities

```text
service_event_subscriptions
  durable subscription configured for a service actor and target

service_webhook_receipts
  provider webhook receipt / dedupe / normalization record

service_event_deliveries
  per-subscription delivery ledger implementing the shared effect contract
```

The service event ledger is not an automation run. It records service-owned event
delivery and the resulting AgentTeam effect.

## 3. Pull request subscription flow

```text
`agentteam exec github|gitea watch-pull-request` or App UI
  -> service capability authorization
  -> create or reuse service_event_subscription

GitHub/Gitea webhook
  -> validate service connection and signature
  -> reserve service_webhook_receipt
  -> match service_event_subscriptions
  -> reserve service_event_deliveries
  -> group compatible deliveries by target and rendered event
  -> write one normal message or task discussion per group as the service member
```

The delivered message source is:

```text
kind = provider_event
refType = service_event_delivery
refId = service_event_delivery.id
```

## 4. Subscription ownership and lifecycle

A pull request subscription is owned by the human who created or authorized it
and by the target chosen at creation time. The service member is the visible
author for later updates; the owner/authorizer remains the routing principal for
notification and permission semantics. Create-or-reuse is idempotent for the
same provider resource, target, owner/authorizer, and event-filter set.

Lifecycle states must cover active, paused, blocked, removed, and failed/invalid
connection cases. Pausing stops future matches. Removing a subscription stops
future deliveries but must not delete already-written messages or task
discussions. Terminal provider states such as merged or closed may produce a
final delivery and should make it easy for agents or users to remove the
subscription when the AgentTeam target no longer needs updates.

## 5. Behavior parity requirements

Pull request subscriptions must preserve existing product behavior:

```text
event filters for comments, reviews, status, synchronize, merge, close, reopen
base branch push opt-in
notify_principal
channel/thread/DM/task-discussion targets
task target canonical scope-fanout discussion delivery (notify_principal carried)
duplicate provider delivery dedupe
delivery idempotency
connection blocking and unblock diagnostics
rate limiting and fanout limits
retention and cleanup policy
pause, resume, remove lifecycle
```

Tests must cover canonical list, identifier, lifecycle, and delivery behavior
without legacy projection or alias rows.

## 6. Webhook and delivery safety

Provider webhook handling must verify the provider connection before matching
subscriptions, reserve a webhook receipt for provider delivery dedupe, and fan out
only to subscriptions whose provider resource, event filter, target binding, and
connection status match. Matching and delivery reservations must be idempotent so
provider retries, AgentTeam retries, and rolling deploy replays cannot create duplicate
messages.

Pull-request webhook matching uses normalized columns on
`service_event_subscriptions` for hot-path predicates:
`external_repository_key`, `base_branch_key`, `target_branch_key`,
`head_sha_key`, and `base_sha_key`. Repository-wide branch and SHA webhook
fanout must use indexed normalized predicates. The JSON `metadata` payload
remains the display/audit payload, not the only match surface for branch or SHA
fanout.

Task discussion delivery appends a canonical `task_discussion` scope message with
the ordinary `scope_fanout` outbox profile and the frozen targeted Task audience,
then returns the committed delivery through `AppWebhookResult.Deliveries` so the
existing `DeliverFollowupMessages` owner performs the first fanout
(lease-coordinated exactly-once for the targeted Task outbox row). Channel,
thread, DM, and task targets must all use the same source projection contract so
history, browser, socket, desktop, and agent prompts display the same provenance.

The same canonical seam is used when an external App answers a
`app_mention` delivery whose bound target is a Task. That reply commits the
durable Task discussion/timeline effect and prepares a store-owned recoverable
post-commit token so `ReplyAppRuntimeDelivery` calls `DeliverCommittedMessage`
only after commit, entering ordinary Task unread/notification and Agent wake. It
is not precedent for a Channel, DM, or reply-thread service effect: those non-Task
messages require an explicit post-commit first-delivery owner and an ordinary
message outbox. The retired Task-only silent/no-outbox delivery path is no longer
used.

When one webhook matches multiple subscriptions with the same target and rendered
event, delivery may be coalesced into one visible message or task discussion
effect while each `service_event_deliveries` ledger row is still completed with
that effect. Coalescing must not merge deliveries that require different target
scopes or different principal notification semantics.

## 7. App notification action delivery

Human actions on `app_interactive_card` messages use the same delivery substrate
with this registered shape:

```text
subscription_kind       = app_notification_action
event_type              = agentteam.app.notification.action
external_resource_type  = app_notification
external_resource_key   = app_notification.id
event_id                 = app_notification_action.id
idempotency_key          = app-notification-action:{action_invocation_id}
```

The delivery envelope carries the App installation/member, notification and
message refs, card revision, action key, validated input, linked business object
version, authenticated Human actor, permission basis, and correlation id.
Callback and runner transports use the ordinary attempt lease, delivery token,
ack/fail, retry, and diagnostics contract.

Action effects include `message_update`, `notification_resolved`, and
`message_withdrawal`. Each effect points at the owning App notification/message
mutation receipt. The linked business domain remains the authority for its
object transition and version.

## 8. Diagnostics

Diagnostics trace:

```text
service definition and installation
service member
target binding
subscription id
webhook receipt id
delivery id
provider event id / delivery key
message or task discussion effect ref
principal / authorizer
failure code and detail
```

They use canonical subscription, webhook-receipt, delivery, and effect ids;
no retired Trigger-run identifier or alias is required.
