# Autonomous message post-commit producer inventory

> Canonical path: `guardrails/message/post-commit-producer-inventory.md`
> Enforced by: `guardrails/message/post-commit-producers.sh`

This is the enforcing inventory for autonomous producers that create an
ordinary Human-visible scope message through the App store without an ordinary
Human/Agent message-create request owning the first post-commit fanout. It also
tracks every production caller of the shared Runtime App append seam.

The inventory exists to prevent the following failure mode: a producer commits
the message and its outbox row, returns success, but nobody performs the first
unread/notification/realtime projection. A new producer cannot be added only by
calling the append helper; it must declare a stable producer contract and an
explicit post-commit owner.

## Canonical ownership seam

- The lowest shared `appendMessageToThread` / `appendMessageToThreadResult`
  seam requires a non-zero `messageAppendLifecycle`. Its complete production
  caller set is executable
  inventory: known request paths choose `messageAppendCallerOwned`, the shared
  Runtime App builder chooses `messageAppendAppServiceContracted`, and the
  autonomous platform writer chooses
  `messageAppendStoreOwnedRecoverable`. A new direct caller cannot reuse one of
  those labels silently; the AST guard rejects any unreviewed callsite.

- `appendAppServiceMessageWithOptions` requires an explicit closed
  `appServiceMessageContract`, constructs the complete browser projection, and
  calls `finalizeAppServiceMessageTx` internally before any successful return.
  It exposes no intermediate stage that a caller can accidentally commit.
- The contract maps a stable producer ID to exactly one owner:
  `appServiceMessageCallerOwned` or
  `appServiceMessageStoreOwnedRecoverable`. App Notification's reviewed
  delivery configurator attaches its DM projection before finalization; every
  other producer must pass nil, so arbitrary post-finalization mutation cannot
  drift the durable snapshot.
- Caller-owned producers persist the delivery payload in the finalizer and
  retain their existing, explicit after-commit handoff.
- Store-owned producers prepare an opaque `postCommitMessage` in the creating
  transaction and call `emitPostCommitMessage` only after that transaction has
  returned successfully.
- `encodeRecoverablePostCommitDelivery` is the only production helper that
  may set `RecoverableStreamProjection = true`. `preparePostCommitMessage`
  persists those exact canonical payload bytes through
  `Outbox.SetPayloadForMessage`, returns the prepared delivery, and copies the
  same bytes into the opaque token. `emitPostCommitMessage` decodes that token,
  so first delivery and durable replay consume one immutable snapshot.

`RecoverableStreamProjection` is a stable `message.created`/optional Inbox
projection recovery capability. It does **not** freeze an ordinary scope's
Human audience and does not make ordinary unread replayable. These ordinary
rows keep status `pending`, not targeted-Task `pending_v2`; only
`HumanRecipientsTargeted` opts into the frozen-recipient unread contract.

## Enforced producer inventory

### Lowest shared append seam

| Raw append caller | Required lifecycle | Downstream owner |
| --- | --- | --- |
| `sendAppMessageForUser` | `messageAppendCallerOwned` | The authenticated request caller receives the committed delivery and invokes shared fanout. |
| `sendAgentThreadMessageResult` | `messageAppendCallerOwned` | The authenticated Agent request caller receives the committed send result and owns shared post-commit delivery. |
| `CreateChannelShareCardForUser` / `ShareChannelShareCardForUser` | `messageAppendCallerOwned` | The Channel API caller invokes shared fanout after commit. |
| `CreateThreadShareCardForUser` | `messageAppendCallerOwned` | The Channel API caller invokes shared fanout after commit. |
| `sendServiceAccountPublisherMessage` | `messageAppendCallerOwned` | The Human-authorized workspace Service Account publish caller invokes shared fanout after commit. |
| `appendAppServiceMessageWithOptions` | `messageAppendAppServiceContracted` | The helper requires the closed App-service producer contract and finalizes the payload internally before returning either a caller-owned delivery or Store-owned token. |
| `DeliverPlatformServiceAccountBroadcast` | `messageAppendStoreOwnedRecoverable` | The app Store prepares a recoverable token in the transaction and emits it after the delivery settlement commits. |

The delivery-returning wrapper forwards its lifecycle into the
freshness-aware result variant. The guard owns the complete production callsite
set for private-agent `WriteConversationMessageResult` and forbids a direct
production call to its convenience `WriteConversationMessage` wrapper. It also
closes both same-package lowercase seams: only
`service_ports.go:WriteConversationMessageResult` may call
`writeConversationMessageResult`, and only that raw writer may call
`writeScopeMessageToConversationResult`. The lower writer must retain its
`SendMessageResult` and its canonical `MessageApplyResult`; a new producer
cannot drop below the exported boundary and discard the post-commit delivery
handoff without failing the AST inventory.

The MessagePlan applier inventory is also structural rather than a file
allowlist:

| Applier | Exact production caller functions |
| --- | --- |
| `ApplyMessagePlanTx` | `serviceEventTaskDiscussionDelivery.appendServiceEventTaskDiscussionTx`, `app.Store.appendChannelMemberEventMessage`, and `message.WriteWorkflowScopeMessageTx`, exactly once each. The Workflow producer uses `MessagePlanSideEffectProfileProductScopeFanout`, persists one canonical `scope_fanout_outbox` record in the caller-owned transaction, and returns the delivery handoff only for the fresh insert. |
| `ApplyMessagePlanTxWithSidecarWriter` | `privateagent.Store.writeScopeMessageToConversationResult`, `taskboard.TxStore.appendTaskScopeMessage`, and `taskboard.TxStore.anchorTaskDescriptionAttachments`, exactly once each. |

Below those exported boundaries, the lowercase physical seam
`message.applyMessagePlanTx` is itself canonical and closed: it may be called
exactly once by `ApplyMessagePlanTx` and exactly once by
`ApplyMessagePlanTxWithSidecarWriter`, with no other production caller. Thus a
same-package producer cannot bypass exported profile/sidecar validation by
calling the implementation directly.

Consequently, merely placing another applier call in
`conversation_write.go`—or any other already-allowlisted file—does not admit a
new message producer. The raw policy is a mandatory boundary declaration, not
permission to omit the next step. The exact caller/function/policy triples and
function-local call counts are checked structurally; adding another raw append
or applier caller fails the fast harness until its owner and recovery semantics
are reviewed here and in the AST inventory.

### Outer caller-owned delivery boundaries

Guarding only the raw Store seam is insufficient: an autonomous producer could
reuse an existing caller-owned method and discard the returned delivery without
adding a raw append call. The AST inventory therefore also closes the current
production callsite sets for these delivery-returning layers:

| Surface | Reviewed callers / handoff |
| --- | --- |
| Message service and App Human-send wrappers (`SendHumanMessage`, `SendMessageForUser*`) | Workspace IM, Task announcement, effect-writer, and Desktop onboarding mock handlers retain the delivery and invoke their existing post-commit fanout; API/testkit adapters only forward the result. Rewind's named notice caller is the intentional projection-only exception. |
| Agent-send wrappers (`SendAgentMessage*`, `SendAgentThreadMessage*`) | Agent message flow retains the delivery/result and runs `afterSentMessage`; the Desktop onboarding mock handler retains the deterministic reply delivery and invokes the shared fanout; other adapters only forward the result. |
| Channel/thread share-card and review-feedback methods | Their Channel handlers retain the returned delivery and invoke shared fanout after commit. |
| `SendSystemMessageForUser*` | Only the internal convenience wrapper is present; there is no reviewed production outer caller today. |
| Approval `postTeamRoleRejectionFeedback` → `SendAppMessageForUserWithOptions` | Explicitly present as the user-deferred known gap below. It is not treated as fixed or as a no-notify exception. |
| Workflow approval legacy replay fallback `sendWorkflowRunFeedback` → `SendMessageForUserWithOptions` | Explicitly inventory-controlled for legacy completed receipts; new decisions use the atomic message/outbox mutation and do not enter this fallback. |

A new call to any of these existing delivery-returning methods fails the guard,
even when it selects an already-reviewed raw lifecycle. Review must classify
the outer caller as post-commit owner, forwarding adapter, intentional named
exception, or explicit deferred debt before the executable inventory changes.

### App-service producer contracts

| Producer | Transactional append/finalize boundary | Explicit first post-commit owner | Recovery / handoff contract |
| --- | --- | --- | --- |
| Provider service event, non-Task target | `internal/store/app/service_event_delivery_dispatcher.go`, `deliverCandidateGroup`; `appServiceMessageProviderEvent` | `appServiceMessageCallerOwned` | The dispatcher returns the committed delivery in `AppWebhookResult.Deliveries`; `internal/api/app/api_workspace_apps.go` hands those deliveries to `DeliverFollowupMessages` after the store call has committed. The transaction persists the ordinary outbox payload, but this producer intentionally keeps its caller-owned first fanout. |
| App Notification | `internal/store/app/app_notification_service.go`, `CreateAppNotification`; `appServiceMessageAppNotification` | `appServiceMessageStoreOwnedRecoverable` | The transaction prepares a recoverable token and the store emits it after commit. A missed/failed first projection leaves the ordinary pending outbox able to replay the stable browser projection, without promising ordinary unread replay. |
| External App runtime reply, non-Task target | `internal/store/app/app_runtime_delivery.go`, `appendAppRuntimeReplyEffect` / `ReplyAppRuntimeDelivery`; `appServiceMessageExternalAppRuntimeReply` | `appServiceMessageStoreOwnedRecoverable` | The reply transaction prepares the token together with the durable App effect receipt; `ReplyAppRuntimeDelivery` emits only after commit. Idempotent reply replay returns no new token. Task targets use the same store-owned recoverable producer: `appendAppRuntimeReplyEffect` delegates to `appendAppRuntimeTaskReplyEffect`, which appends through `appendServiceEventTaskDiscussionTx` and prepares the identical token via `preparePostCommitMessage` with `committedMessageProducerExternalAppRuntimeReply`; `ReplyAppRuntimeDelivery` then calls `DeliverCommittedMessage` only after commit and the lease-coordinated Task inline fanout owns first delivery. |
| Knowledge Q&A assistant reply | `internal/store/app/knowledge_qa_messages.go`, `AppendKnowledgeAssistantMessageTx`; `appServiceMessageKnowledgeQA` | `appServiceMessageCallerOwned` | The Knowledge Q&A worker commits the assistant message with the turn completion, then explicitly calls its `WorkerMessageFanout.DeliverCommittedMessage` owner after the transaction. The finalizer persists the ordinary outbox payload. |
| Platform Service Account broadcast | `internal/store/app/platform_service_account_broadcast.go`, `DeliverPlatformServiceAccountBroadcast`; direct `preparePostCommitMessage` / `emitPostCommitMessage` | Store-owned recoverable canonical seam | This producer has its own specialized transactional writer rather than `appendAppServiceMessageWithOptions`. It prepares the same opaque recoverable token with its audit/delivery commit and emits it only after the coordinator transaction succeeds. |

The process-level `CommittedMessageFanout` dependency is installed in
production server wiring (`internal/server/wire.go`), the executable E2E server
(`cmd/agentteam-e2e-server/main.go`), and the API integration runtime
(`internal/api/testkit/runtime.go`). The E2E server has no unread runtime, but
it must still perform the first stable realtime projection instead of silently
waiting for replay. The executable guardrail owns the complete production
callsite sets for the setter, prepare, encode, finalize, and emit functions.

## Intentional no-notify exceptions

These reviewed paths must not be “fixed” by routing them into ordinary
unread/notification fanout. Their source-level allowlist is
`direct-no-outbox-baseline.md`.

| Path | Intentional behavior |
| --- | --- |
| Rewind — `internal/api/im/rewind_handler.go` | After the durable rewind, publishes a direct `conversation.rewound` invalidation and a Human-visible system notice's direct `message.created` projection. By product decision neither enters ordinary unread, notification, or Agent-wake fanout; rewind facts and the notice row remain recoverable through conversation history/refetch. |
| Channel member events — `internal/store/app/channel_member_events.go`, `internal/api/channel/api_channel_membership.go`, `internal/store/app/agent_lifecycle.go`, and `internal/service/channeladmin/service.go` | Persists member add/remove system transcript rows with `MessagePlanSideEffectProfileNoOutbox`. The Channel API, standalone `CreateAgentForUser`, and Agent Exec channel-management service publish only the named projection-only `message.created` presentation after commit; no ordinary unread/notification/Agent-wake fanout is applied. Agent creation accumulates every explicit initial-channel result, covering regular creation, duplication, and role materialization. `CreateWorkspaceAgentInTx` exposes those transaction-bound results to its caller instead of emitting inside a caller-owned transaction; rollback must discard them. Agent Exec retains its mutation effects inside `WithTx`, publishes them only after the transaction returns success, and emits nothing for rollback, no-op, conflict, or exact receipt replay. The AST inventory closes all production `CreateAgentForUser` callers, locks `createAgentInWorkspace` to `CreateAgentForUser` plus `CreateWorkspaceAgentInTx`, locks `addAgentChannelMembershipsInTx` to `createAgentInWorkspace`, requires every append result to be retained, requires the standalone Agent-create emit to follow commit, and requires the sole Agent Exec effect emit to remain outside and after its transaction success guard. It currently admits no production `CreateWorkspaceAgentInTx` caller. |
| Hidden task-description attachment anchor — `internal/store/taskboard/detail_mutation.go`, `anchorTaskDescriptionAttachments` | Persists a hidden `task_description_attachment` authority row and attachment claims through `ApplyMessagePlanTxWithSidecarWriter`. It is not a Human-visible transcript event and intentionally emits no unread, notification, realtime message, or Agent wake. |

Reaction fanout is a separate direct post-commit exception that does deliver a
reaction signal; it is not one of the no-notify paths above and remains governed
by `direct-no-outbox-baseline.md`.

## Reviewed but deferred: Approval Team rejection feedback

`internal/service/approvalapp/team_role_feedback.go` sends Team-role rejection
feedback through `SendAppMessageForUserWithOptions`. That path is outside the
contracted Runtime App seam and is **NOT remediated by this guardrail**. Its
post-commit notification ownership remains explicitly deferred follow-up work;
listing it here is not an allowlist exception and must not be read as a claim
that its delivery behavior has been fixed.

## Enforcement procedure

Run the structural guard whenever changing an autonomous producer, the shared
App-service append boundary, committed-message fanout wiring, or recovery
policy:

```sh
guardrails/message/post-commit-producers.sh --check-only
```

`scripts/harness-fast.sh` runs this check together with the direct/no-outbox
allowlist, so the repository fast harness rejects an unreviewed producer or
exception before merge.

Use inventory mode to print this contract together with the current production
AST-controlled callsite surfaces:

```sh
guardrails/message/post-commit-producers.sh --inventory
```

The guard parses production Go under `internal`, `cmd`, and `desktop`
(excluding tests). It rejects an unreviewed append/finalize/prepare/emit/setter
caller, an unreviewed exported or lowercase privateagent writer caller, an
unreviewed MessagePlan applier caller or function-local count drift, an
App-service append without its exact contract/configurator policy, a
contract-to-owner mapping drift, an Agent-create raw-seam caller drift,
construction of non-empty handoff tokens outside their canonical helpers, or a
second production writer of `RecoverableStreamProjection`. Updating this file
alone never admits a new producer: the executable inventory must also be
changed deliberately.
