# Task Timeline Messageization

> Canonical path: `specs/task/task-timeline-messageization.md`
> Status: living-spec
> Scope: task timeline write/read authority, message-backed timeline items, typed activity projections, task-body identity presentation, and guardrails that keep task activity writes unified.
>
> Related specs:
>
> - [`specs/collaboration/source-projection-delivery.md`](../collaboration/source-projection-delivery.md)
> - [`specs/task/task-scope.md`](./task-scope.md)
> - [`specs/task/board.md`](./board.md)
> - [`specs/message/scope-model.md`](../message/scope-model.md)
> - [`specs/message/delivery-ledger-optimization.md`](../message/delivery-ledger-optimization.md)

## 1. Purpose

The task timeline is the canonical task detail feed. It combines human-readable task-scope messages with structured task facts without creating a second chat system, a hidden message copy, or a scattered activity-write path.

Task timeline messageization means every timeline item is planned through one domain contract before storage side effects happen:

- content that is a task-scope message is message-backed and uses message authority;
- state, audit, and fact changes that are not messages are typed activities and use task authority;
- projection into `scope_activity_events` is centralized and never written directly by feature code;
- side effects are explicit in the plan and must not silently expand history, unread, delivery, wake, or visibility behavior.

## 2. Timeline Item Authority

| Item kind | Authority | Storage shape | Examples |
| --- | --- | --- | --- |
| Message-backed timeline item | Message subsystem | `scope_messages` plus message-owned refs, reactions, search, sync, and delivery side effects | task discussion, agent or human updates posted into the task scope, service-event task discussion messages |
| Typed activity | Task subsystem | task fact tables and a centralized `scope_activity_events` projection with `activity_ref_type = 'task_event'` | created, assignee changed, status changed, blocked, checkpoint added, artifact added |

Message-backed timeline items must read their body, author, contribution source/provenance, timestamps, reactions, attachments, search text, and message visibility from message authority. Typed activities must read their task fact payload and activity ordering from task authority and the centralized activity projection, while exposing the same browser/API identity/provenance substrate as messages and reactions: canonical `authorProfile` plus typed `source`. Legacy `actorRef` / `actorDisplayName` / `sourceKind` / `sourceMessageId` / `sourceActivityId` fields may remain only as adapter-derived compatibility fields.

A timeline reader may merge these item kinds, but it must not treat a typed activity row as a message row and must not copy message-backed content into a task-only activity payload.

## 3. TaskTimelinePlan Write Contract

Task mutations that affect the timeline must use the owning domain plan before writing timeline side effects:

- Message-backed task-scope writes must use the message plan/apply path and an explicit message side-effect profile.
- Message-backed timeline ordering must be declared through `TaskTimelinePlan.TypedActivityIntents` with `source_message_id` pointing at the owning message. The intent may carry only stable ids and typed metadata; the body stays in `scope_messages`.
- Pure task state/audit events use `TaskTimelinePlan.TypedActivityIntents` without a source message.
- Task-owned fact writes such as checkpoint, artifact, review, or task status state stay in their task tables; after the fact write, task mutation code declares only a typed event intent.
- Summary timestamp/counter touches stay request-bounded and explicit in the task mutation.

Feature code may create facts and choose the event intent, but it must hand the side-effect plan to the task timeline writer. It must not allocate task event ids, allocate activity sequence numbers, insert `scope_activity_events` rows, or write Task attention / Inbox state itself.

## 4. Typed Activity Projection Writer

Typed activity projections are centralized in the task timeline writer. The writer owns:

- task-event id allocation for typed activity projections;
- trimming and validation of task id, actor ref, event type, payload, and timestamp;
- optional task-scope actor participant materialization according to the plan side-effect profile;
- per-task-scope activity sequence reservation;
- insertion of `scope_activity_events` rows with task-event payload metadata;
- for pure typed Activity, application of the shared Task attention plan and one `activity_event` outbox record with a stable recipient snapshot.

The writer may support multiple actor-participant modes because existing write paths have different side-effect profiles. For example, ordinary taskboard writes preserve the existing default-quiet participant behavior, while service/app-authored Task discussion appends preserve an explicit no-extra-membership actor mode with canonical message fanout. These modes are part of the unified plan, not bypasses or fallback paths.

## 5. Side-Effect Discipline

Task timeline writes must be request-bounded and side-effect explicit.

- Message-backed task discussion may use message fanout when the product action is an ordinary task discussion send.
- Service/app-authored Task discussion appends (provider service events and external App runtime replies) write a canonical `task_discussion` scope message with `MessagePlanSideEffectProfileScopeFanout` and one ordinary `scope_fanout` outbox job plus a typed `discussion_added` activity projection. Provider events return the delivery so `DeliverFollowupMessages` owns first fanout; external App runtime replies prepare a store-owned recoverable post-commit token and call `DeliverCommittedMessage` only after commit. The retired Task-only silent/no-outbox profile is no longer used.
- Checkpoint timeline items are typed task activities/facts; they do not create task-scope message rows, message-created history entries, or message-backed content that would require historical backfill. They can produce targeted Task attention through the Activity outbox.
- Typed activities must not create ordinary message rows. Pure typed Activity uses exactly one `activity_event` outbox when its committed recipient snapshot is non-empty.
- Actor participant materialization must be controlled by the plan side-effect profile and must not silently widen visibility or wake behavior.
- Search, sync, read cursor, unread, notification, and delivery behavior must come from the owning authority for the item kind and enter shared projection writers through explicit intents.
- Task discussion append is a message-backed send. Its search document, sync dirty marker, reactions and only attention outbox are message-owned effects; the paired `discussion_added` Activity is timeline-only. Taskboard may supply task authorization and task-scope context, but it must not hand-roll `scope_messages`, `scope_message_search_docs`, `scope_message_reactions`, or Task unread/Inbox writes.
- Create-with-assignee, assign, claim, reassign and unassign write canonical assignee facts and typed Activity. Agent delivery / wake is derived from `explicit watchers ∪ current assignee − actor` for that Activity, not from a public assign/start dispatch mode and not from a hidden `task_dispatch` message.

## 6. Forbidden Write Shapes

The following shapes are not allowed:

- feature-local helpers that append task events outside the task timeline writer;
- direct `scope_activity_events` inserts from taskboard, app, service-event delivery, API, or schedule feature code;
- fallback or compatibility branches that write both old and new activity paths;
- hidden message rows that mirror typed activity payloads;
- typed activity payloads that become the authority for message-backed content; checkpoint payloads are allowed because checkpoint is a typed task activity, not message-backed content;
- ad hoc no-outbox message writes outside an explicit message plan side-effect profile.

## 7. Read Contract

The task timeline read service merges message-backed items and typed activities at the presenter/read-model boundary:

- message-backed items fetch message-owned fields from message authority;
- typed activities fetch task-owned fields from task authority and activity projection metadata, hydrate actor identity into canonical `authorProfile`, and expose activity provenance as `source`;
- dedupe happens in the timeline read/projection layer, not by duplicating writes;
- old typed activity rows are interpreted only by the canonical timeline read/projection layer; checkpoint rows remain typed activity rows and are not message-hydrated.

The browser-facing task detail contract exposes a single `timeline` DTO/page. The task detail UI must consume that single timeline DTO and must not fetch or merge parallel `activity` and `discussion` arrays.

Task detail and the unified timeline page may be transported independently without creating separate discussion/activity authorities. Detail requests that omit `includeTimeline` or set it to `true` retain the legacy embedded first page for older clients. New clients set `includeTimeline=false`, render the remaining Task detail independently, and load the same unified page through `GET /api/tasks/:taskId/timeline`.

Timeline reads must bound ordered message and typed-Activity candidates to the requested page window before compatibility JSON interpretation, dedupe, and actor/profile joins. Canonical message-backed rows are correlated through `scope_activity_events.source_message_id`; JSON payload lookup is a legacy fallback, not the primary join authority. New opaque continuation tokens carry the activity sequence used by the merged projection so both source candidates can apply keyset bounds. Tokens without that sequence remain readable through the legacy continuation path during the compatibility window.

Task body identity uses the same principal/profile substrate as message, reaction, and task activity identity. The canonical Task payload fields are `createdByProfile` and nullable `assigneeProfile`, each carrying an `AuthorProfile`-compatible profile. Canonical task body provenance is `source`, a message `ContributionSource` derived from the task source-thread anchor plus `createdByProfile`; `sourceThread` remains the navigation/jump-back projection and is not the provenance source of truth. Conversation/Inbox task metadata uses the same `assigneeProfile` shape. The public Task contract must not expose `currentAgent`, `claimedBy*`, `suggestedAssignee`, Task-row `reviewer`, their profile variants, compact actors, or fallback aliases. Review Request reviewer identity remains an independent review fact: requesting or reassigning review preserves the Task assignee and must not emit an `assigned` Task activity. Task list/detail/related-row/conversation-row readers hydrate assignee from the canonical member projection.

Task discussion mutation is not a second message system:

- The canonical server-side write command is task-scope message send (`SendTaskScopeMessage` / `/api/messages` with a task scope target). It resolves task scope, authorizes the writer, and writes an ordinary task-scope message through the message plan/apply path. Browser/API callers must use this message endpoint for task discussion sends; the task-native `POST /api/tasks/:taskId/discussion` product shortcut is removed rather than kept as a facade over the canonical send command.
- Old daemon and tool compatibility may keep their legacy target/wire contracts only as adapters into the canonical task-scope message send command. Taskboard, task service, browser message, and agent send ports must not expose a separate first-class task-native append writer.
- Task routes and tools use the canonical assignee contract. Old top-level Task responsibility aliases are deleted rather than derived from `assigneeProfile`. New raw task-scope discussion sends use the message API route (`send` with a raw `#tsk_...` target) and therefore follow the message API daemon-version gate; task routes must not become a hidden facade around a second task-native discussion writer.
- Task discussion read models are message read models: the canonical browser DTO is `authorProfile + source` using the message `AuthorProfile` / `ContributionSource` substrate. Legacy `createdBy` may remain only as an adapter-derived compatibility field for older task clients; it must not be the only author field or the source of truth.
- Edit and delete are not task discussion product capabilities; there are no task-native edit/delete routes, DTOs, store methods, or UI actions.
- Reactions use the ordinary message reaction surface (`/api/messages/:messageId/reactions`) and message reaction DTO/cache behavior. The task detail discussion timeline must call message-owned reusable reaction controls that own the ordinary message reaction bar, reactor popover, picker, copy, and cache mutation semantics; it must not keep task-local reaction wrapper state, picker/drawer/popover glue, task-specific reaction chips, quick-reaction helpers, or task-owned reaction locale keys. Taskboard must not expose task-native reaction mutation routes or write `scope_message_reactions` directly.
- Search projections for task discussion messages remain message-owned effects. Targeted Task Message and typed-Activity attention contributes to the same Human Task Inbox row and UnreadRuntime entry. Neither path may write retired `conversation_member_states` task unread/inbox counters or call a restored taskattention compatibility path.
- Task typed activities remain non-message activity rows, but their browser/API/agent DTOs use canonical `authorProfile + source` for identity/provenance. Legacy `actorRef` / `actorDisplayName` / `sourceKind` / `sourceMessageId` / `sourceActivityId` are compatibility fields derived from canonical `AuthorProfile` / `ContributionSource`, not the runtime read source of truth.

Read pagination for the task detail feed is `GET /api/tasks/:taskId/timeline` only. Server-side task service, store, API contracts, and task-service HTTP clients must not expose independent `GetTaskDiscussion` / `GetTaskActivity` read authorities or compatibility wrappers. Old daemon compatibility is outside this task-service split-read surface and must derive any legacy fields from canonical message/timeline data.

Readers must not resurrect deleted/suppressed messages through activity payloads and must not infer message delivery state from typed activities.

## 8. Guardrail

`guardrails/task/task-timeline-write-boundary.sh` protects the task timeline, attention, search, unread, and identity boundaries. It rejects scattered task event append helpers and direct `scope_activity_events` inserts outside the centralized task timeline writer, rejects server-side split task timeline read entrypoints, rejects task-native discussion append/edit/delete/reaction mutation ports, requires canonical `SendTaskScopeMessage` wiring, requires pure typed Activity and Task Message to carry exactly one stable attention recipient snapshot, rejects `discussion_added` double fanout, rejects restored taskattention / Task Inbox physical counters, requires canonical `assigneeProfile` across Task store/API/Web surfaces, and rejects old Task responsibility aliases or compatibility fallback.

## 9. Strict Cutover Audit Evidence Matrix

Task discussion messageization is a strict cutover, not a rename/refactor and not a facade over the previous task-native discussion implementation. Each runtime path below names the canonical source of truth and the only allowed legacy boundary.

| Runtime path | Canonical source of truth | Allowed legacy/compat boundary | Required proof |
| --- | --- | --- | --- |
| Append / mutation | `SendTaskScopeMessage` is the canonical command: it resolves task scope, authorizes the writer, and uses the message plan/apply path to create a task-scope `scope_messages` row, message search doc, sync dirty marker, fanout/outbox, attachments, and generic message unread semantics. | Old daemon/tool target compatibility may adapt into `SendTaskScopeMessage`; browser/API task discussion sends use `/api/messages` with the task scope id. The legacy task-native `POST /api/tasks/:taskId/discussion` shortcut and `AddTaskDiscussionEntry`-style writers are deleted. Edit/delete task discussion capabilities are deleted. | Guardrail requires `SendTaskScopeMessage` wiring, rejects taskboard-local `scope_messages`, search, reaction, unread writes, rejects legacy task discussion mutation routes/DTOs/ports, and semantic tests post through `/api/messages` task-scope sends and read back message-backed timeline entries. |
| Detail/timeline read model | `scope_messages` is the runtime row authority. Authors come from `scope_messages.author_member_id` plus message author projection (`members`/`users`/`agents`/`service_members`/`service_definitions`). Provenance comes from typed `ContributionSource` resolved by the message contribution-source resolver. | `createdBy` is a compatibility field derived from canonical `AuthorProfile`; it is not a read-model source of truth. | Guardrail requires `AuthorProfile + source`, `scope_messages sm`, `sm.author_member_id`, `AttachContributionSourcesForAppMessages`, service actor joins, and explicit compatibility actor helpers; tests assert read-back `authorProfile/source` and that legacy `createdBy` follows `AuthorProfile`. |
| Browser/API/agent/MCP/internal adapters | `TaskDiscussionEntry.authorProfile` and `TaskDiscussionEntry.source` are canonical DTO fields on browser-facing task payloads and agent/MCP task payloads. | `createdBy` may be emitted for older task clients only; alias direction is canonical `AuthorProfile -> CreatedBy`, never `CreatedBy -> AuthorProfile`. | Guardrail requires adapter helpers (`taskDiscussionCompatActorFromAuthorProfile`, `taskboardCompatActorFromAuthorProfile`) across browser, internal task, and agent task adapters, and forbids DTOs that only expose `TaskActor`; converter tests intentionally mismatch fallback `CreatedBy` and expect canonical author-derived compatibility output. |
| Web render/cache | Task detail timeline renders discussion identity through message author helpers and updates reactions through ordinary message reaction cache mutation. | `createdBy` can exist in the wire type for old payloads, but production task detail rendering must not read it as the author source. | Guardrail requires `resolveMessageAuthorIdentity(...)` in task timeline/summary and rejects production reads of `entry.createdBy.*`; Web tests cover timeline, summary, discussion panel, and shared cache behavior. |
| Task body, collaborator, and fact identity/provenance | Task list/detail/related rows and Conversation/Inbox task metadata hydrate canonical `createdByProfile` and nullable `assigneeProfile` from member/user/agent/service-author projections. Task list/detail/related rows also expose canonical `source`. Review history keeps its own `requestedByProfile` / `reviewerProfile` / `decidedByProfile`; those are Review Request facts, not Task-row responsibility fields. | No top-level `currentAgent`, `claimedBy*`, `suggestedAssignee`, Task-row `reviewer`, compact actor, or profile alias is allowed in public/runtime contracts. During the bounded zero-downtime rolling window only, the Task store persistence adapter may translate between canonical assignee and retained legacy physical columns so old and new nodes sharing one database remain mutually visible. | Guardrail requires `assigneeProfile` across store/API/browser/agent/conversation contracts, confines legacy responsibility symbols to migration and the explicitly named rolling persistence adapter, and verifies Human / Agent assignees hydrate through the same principal projection. |
| Typed task activity write/read/attention plan | Non-message task facts remain `scope_activity_events(activity_ref_type = 'task_event')`; mutation code writes facts and declares `TypedActivityIntent`, while the timeline writer owns event id allocation, sequence, participant setup, activity insertion and the pure-Activity attention intent. The recipient snapshot is `watchers ∪ current assignee − actor`; Human recipients update the unified Task conversation unread and Agent recipients enter delivery/wake. | Typed Activity payloads remain task facts and must not be copied into `scope_messages`. Retired taskattention/counters may not be restored. During the rolling window the same `scope_notification_settings` table has a temporary Task-only v2 Watch authority, so an old binary overwriting legacy `state` cannot create a new explicit Watch. | Tests assert typed Activity writes one `activity_event` outbox, recipient union/dedupe/self-filter, Human unread, Agent wake, replay idempotency, no synthetic message, and old-node legacy notification writes cannot overwrite the v2 Task Watch decision. |
| Task assignment / Agent delivery | Create-with-assignee, assign, claim, reassign and unassign write canonical assignee facts plus typed Activity. Agent attention is derived from the same event recipient snapshot used for Human attention. | No public create/assign/start mode, hidden `task_dispatch` message, legacy responsibility alias, or taskdispatch-only recipient policy remains on a new canonical write path. `MessageTypeTaskDispatch` is registered as a legacy read/replay-only receive token solely so rolling nodes can drain two old queue families: hidden message-source `ScopeFanoutPayload` rows and `dispatch_requested` activity-event `ScopeDeliveryIntentPayload` rows. Generic and named active writers must reject the token. | Tests prove create has one schema with optional assignee, claim cannot take over another assignee, assignment Activity wakes a non-actor Agent recipient, no new hidden message or double outbox is created, and literal persisted v1 payloads from both old queue families replay agent-only without Human realtime or unread effects. |
| Reaction UI/API | Ordinary message reaction controls and `/api/messages/:messageId/reactions` own reaction UI, picker, reactor popover, API, and cache semantics. | No task-specific reaction route, wrapper, chip, quick reaction helper, or locale copy remains in production. | Guardrail requires message-owned `MessageReactionControls`, `MessageReactionBar`, and `ReactionPickerContent`, and rejects task-local reaction glue/copy. |
| Search and unread | Message search remains message-owned. Task profile search remains owned by `internal/store/taskboard`. Targeted Task Message and typed Activity attention both update the same member-local Task conversation and UnreadRuntime entry; mark-read advances both cursors. Owner-local runtime events retain bounded per-Task message/activity sequence tails so a delayed read callback cannot erase attention committed after its cursor. | Code must not write or read retired `conversation_member_states` task counters/projection columns. The Inbox Tasks category is a filter over the unified conversation index, not a new store. If more than the bounded exact tail is compacted and a delayed cursor lands inside that opaque prefix, unread is conservatively retained rather than undercounted; the next read cursor covering the compacted frontier (or ordinary recovery) converges it. | Tests assert watcher/assignee/mention targeting, Message + Activity unread aggregation, delayed-read ordering, bounded-tail conservative compaction, mark-read, one-row-per-Task, and no broad participant fanout or legacy projection. |
| Version pinning / mutable external facts | Task discussion author/source reads use canonical message/service-author facts. Task assignee reads use the canonical member FK and principal projection. | Public API/daemon/browser adapters never expose historical responsibility shapes; stale browser clients refresh rather than receiving Task wire aliases. Expand migration retains legacy physical columns, and one store-layer compatibility adapter performs the minimum dual-read/write needed while old and new backend nodes coexist. The same-table Task Watch v2 bridge and old async payload receiver are the only other rolling boundaries. After fleet convergence, a separate online contract release deletes those adapters and temporary columns. | Migration and mixed-version tests cover initial backfill plus old-node post-backfill assign/reassign/unassign and legacy notification-state overwrite; runtime guardrails reject legacy Task fields outside migrations, fixtures, the temporary persistence adapter, and the bounded old-payload receiver. |
| Legacy physical tables / old files | The old task-native discussion mutation/reaction/read implementation is not a source of truth. In particular, production runtime paths must not read `task_discussion_entries` or `task_discussion_reactions` as the discussion source. | Compatibility wrappers, migration backfill, and test fixtures may mention old names only when explicitly scoped and documented. | Guardrails reject old production routes/ports/helpers, runtime reads from legacy physical tables, and stale spec/cache inventory claims; negative validation should demonstrate that reintroducing a removed route/helper/table read fails. |

Reviewers must audit from `upstream/main`, not only the latest diff. A finding that a guardrail allows a forbidden direction (for example canonical code calling old helpers, alias direction `new -> old`, runtime reads from legacy tables, stale guardrail/spec inventory, or an untested projection write/read agentteam) is a blocker even if product behavior appears unchanged.
