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

76 lines
8.1 KiB
Markdown
Raw Normal View History

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