Submatter/.convoys/workspace-chat-foundation/brief-1-chat-persistence.md

8.1 KiB

brief convoy role title status priority slice_dependencies recommended_model model_tier
1 workspace-chat-foundation role-implementer Durable chat persistence and workspace API in_progress P1
auto/coding 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.

  • 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.