--- brief: 1 convoy: workspace-chat-foundation role: role-implementer title: Durable chat persistence and workspace API status: in_progress priority: P1 slice_dependencies: [] recommended_model: auto/coding model_tier: standard --- # Brief 1 — `Task-chat-persistence.md` ## Execution checkpoint — 2026-10-22 - Shared command/context validation and the fail-closed authorization policy are implemented and accepted after standards/spec review; `@tasks/shared` has 65 passing tests. - The tenant-scoped chat schema and generated migration are implemented and accepted after standards/spec/migration review; database tests pass. A separate generated `0018` repairs pre-existing omitted budget tables, and chat is isolated in generated migration `0019`. - The concrete Drizzle human send/reply transaction, mention-aware idempotency, atomic sequence allocation, validated `chat.message.created.v1` outbox event, and workspace-authorized `chat.send` tRPC procedure are implemented and review-clean. Acceptance remains blocked on the live PostgreSQL integration gate below; channel lifecycle, history, edit/tombstone, mention counts, and read-state operations are pending. - **Blocking integration gate:** against disposable PostgreSQL with migrations through `0019`, verify advisory-lock blocking under simultaneous same-key sends; exactly one message, mention set, logical outbox event, and sequence increment; full rollback of the sequence/message/mentions/outbox after an injected post-allocation failure; and Drizzle's runtime `bigint` shape for `UPDATE … RETURNING next_sequence - 1`. Unit fakes verify orchestration only and must not be cited as transaction/concurrency proof. - IA/UX review completed; private-object authorization remains blocked, and agent approval semantics and AI side-panel convergence require resolution before their dependent integrations ship. ## Goal Implement the durable, tenant-scoped channel/message foundation and workspace-authorized tRPC API described by `plans/Plan-workspace-communications/Epic-workspace-chat/Task-chat-persistence.md`. ## Files Expected scope; confirm exact module names against current package exports before editing. - `packages/database/src/schema/` — add chat channel, membership, message, read-state, and the outbox contract schemas and export them. - `packages/database/migrations/` — generate a new migration and metadata; never edit an existing migration. - `packages/shared/src/` — add reusable zod schemas/types for chat commands, author/message states, and the versioned outbox event where cross-process sharing is needed. - `apps/web/server/routers/` — add a `workspaceProcedure` chat router for channel list/create/archive, membership management, history/send/reply/edit/delete, and mark-read. - Adjacent `*.test.ts` files — authorization, provisioning, pagination, idempotency, thread, and cross-workspace isolation coverage. ## Required design - Every tenant-owned table has non-null indexed `workspace_id`. - Channel types are `public`, `private`, and `direct`; visibility is separate from context. This brief accepts only an explicit workspace context (including a documented workspace default for existing callers); reject object references/unknown context keys rather than dropping them. Add a shared authorization seam with workspace AND context AND channel checks; do not implement a project ACL in this brief. - Public workspace channels are readable by active workspace members; private/direct channels additionally require active channel membership. Every list/title/preview/history/thread/mention/count/read-state query uses the same access predicate. Archived channels permit authorized reads only. - Active members create workspace public/private channels and become channel owner. An authorized channel owner or workspace `admin`/`owner` can manage that channel only with channel access; role alone is not a private/direct read bypass or self-join mechanism. All membership targets must themselves be authorized. Direct-channel participant sets are immutable; changes resolve a different exact-participant channel. Object/private-project access remains blocked until an authoritative context policy exists. - Message authors may edit their own non-deleted messages. They may tombstone their own messages; channel owners and workspace `admin`/`owner` may tombstone messages for moderation. System/agent edits require the owning service path, not an impersonated user mutation. - Channel lifecycle includes public/private creation, rename/archive, private membership management, and idempotent direct-channel creation for an exact sorted participant set. - Message authors use a discriminated user/agent/system representation without fake user rows. - A client-generated idempotency key is unique within the workspace, channel, and author scope. - Every message receives a transactionally allocated, per-channel monotonic sequence. History and reconnect cursors are `(channel_id, sequence)`; message ID breaks ties only if allocation semantics ever permit one. - Sending a message or thread reply inserts its versioned outbox event in the same transaction. Brief 1 owns this boundary; Brief 2 consumes the outbox. - Threads are one level. Channel history returns root messages only plus reply counts/latest-reply metadata; thread history returns direct replies ordered by the same channel sequence; replies cannot themselves receive replies. Every root reference must resolve inside the same workspace and channel. - Deletes are tombstones where thread/history integrity requires them. - Read state stores the highest intentionally viewed message sequence per member/channel. `markChannelRead` may only advance it and inserts a read-state invalidation event for other devices. - Mention parsing creates validated, workspace-scoped persisted message-mention rows in the message transaction. This convoy derives unread mention counts from those rows above the recipient's read cursor; the notifications epic later creates inbox projections from the same source events. - All reads and mutations authorize through `ctx.workspace` plus the channel-type and role rules above. ## Scope checkpoint and acceptance Next implementable chat brief: this one. Start with failing policy tests and schema contracts, then migration and workspace-only router/outbox. Existing `objects` includes projects but has no private-object ACL: do not invent project memberships or authorize by `workspace_id` alone. See [context follow-on](../../plans/Plan-workspace-communications/Epic-workspace-chat/Task-contextual-chat-authorization.md). - Same-workspace unsupported object contexts fail closed; no fallback public channel is created. - Nonmember workspace admins cannot read/self-add to private/direct channels via administrative endpoints; counts/previews cannot reveal them. - Access removal is respected by subsequent queries/writes; stale membership cannot override context denial. - Foundation mention parsing cannot install/add/invoke agents; the agent-channel-membership task owns reviewed addition. - Duplicate send retries result in one persisted message. - Cross-workspace and unauthorized private-channel requests return non-disclosing errors. - Stable cursor pagination works for forward catch-up and backward history. - Channel creation, rename/archive, private membership changes, immutable exact-participant direct channels, and message edit/moderation enforce the documented ownership and workspace-role rules. - One-level thread roots/replies, persisted mention rows/counts, and durable per-member read state have working API coverage. - `markChannelRead` only advances the per-channel sequence and atomically emits its invalidation event. - Message and thread-reply commits atomically include the corresponding outbox event. - `pnpm db:generate`, `pnpm lint`, `pnpm type-check`, and relevant tests pass. ## Prompt Read `AGENTS.md`, `docs/Glossary.md`, database schema exports, `apps/web/server/trpc.ts`, and `apps/web/server/lib/resolve-workspace.ts` before writing. Use TDD for authorization and idempotency. Keep this brief limited to durable persistence/API and atomic outbox writes; do not implement outbox publication, WebSockets, or UI.