Problem
A document comment fails to send when its heading has no channel row yet. The chat creates that row only on some opens. It decides from client state, not from the server.
On 14ab7f9c1. Webapp paths are under apps/webapp/src/. Paths that start with packages/ are from the repo root.
- A heading chat gets its channel row when it first loads.
useChannelMetadata calls upsertChannel only when workspaceSettings.channels has no entry for the heading id (components/chatroom/hooks/useChannelMetadata.ts:29-44). That map is client state: mode memory, the member flag, and channelInfo. It is not a record of channel rows.
- A comment start writes an entry before the chat loads.
openHeadingChatroom with intent: 'comment' calls setCommentMessageMemory(headingId, …) (services/openHeadingChatroom.ts:137-145). Then it schedules the pane (:153). setMemory creates the entry when it is missing (stores/chat/workspaceSettingsStore.ts:93-102). So when the chat loads, the check passes and the create step is skipped. When the pane is already open on that heading, it only focuses the composer (services/openHeadingChatroom.ts:148-151). That chat has already run its load.
- Every load also writes an entry, even when the row is missing (
stores/chat/bootstrapStore.ts:24-27). So after a create step that failed, a reopen of the same heading chat in the tab never runs it again.
- With no row, the metadata RPC returns one row with
channel_info null (packages/supabase/scripts/10-functions.sql:94-113). AccessControl shows the composer when channelInfo is missing (components/chatroom/components/ChannelComposer/ChannelComposer.tsx:39).
- The comment insert then fails. The insert policy needs
internal.can_read_channel(channel_id) (packages/supabase/scripts/13-RLS.sql:211-216). That check needs the channel row (packages/supabase/scripts/10-0-func-helpers.sql:143-162). The submit catch shows the error in a toast (components/chatroom/components/MessageComposer/hooks/useComposerSubmit.ts:172-179).
Steps to reproduce
- Sign in and open a pad. Add a new heading with a line of text under it. No user has opened its chat, so it has no channel row. Keep the chat closed.
- Reload the page, so the chat store is empty.
- Select text under that heading and choose Comment.
- Type
first comment and press Enter.
- See an error toast. No comment is added.
public.channels has no row with that heading id.
- Control: reload, open the same heading chat from the table of contents, then comment. The comment sends.
Acceptance criteria
Agent Brief
Category: bug
Summary: The chat must create a heading's channel row when the server reports it missing, whatever the client store holds.
Current behavior:
useChannelMetadata treats an entry in workspaceSettings.channels as proof that the channel row exists. A comment start writes that entry before the chat loads, so the create step is skipped. Every load writes an entry too, so a failed create step is never retried in the tab. The metadata RPC then returns no channel, the composer shows, and the comment insert fails on the messages insert policy.
Desired behavior:
The chat creates the channel row whenever the row is missing, as the server reports it. A comment on a heading with no chat sends on the first try, like a message after a table of contents open does today.
Key interfaces:
useChannelMetadata() — the create gate, and the load that runs the metadata RPC.
workspaceSettings.channels — per-channel client state. Its entries are not channel rows.
setCommentMessageMemory() and setMemory() — write an entry before any load.
bootstrapChannel() — writes an entry on every load, and channelInfo only when the row exists.
get_channel_aggregate_data — returns channel_info null when the channel row is missing.
upsertChannel() — an upsert with onConflict: 'id' and ignoreDuplicates: true. It returns no row when the row already exists.
openHeadingChatroom() with intent: 'comment'.
AccessControl — shows the composer when channelInfo is missing.
Out of scope
Notes
Use channel_info from the metadata RPC as the missing-row signal, as #292 also advises. The RPC already runs on every open. After the create step, run the metadata RPC again, and store the channelInfo it returns, so AccessControl gets the channel type. The create trigger already adds the creator as an admin member (packages/supabase/scripts/10-2-func-channels.sql:16-44). A join after that fails in the duplicate-member trigger (:345-371). So join only when the RPC still reports no membership. That happens when another user created the row first. Do not move mode memory out of workspaceSettings.channels to fix this.
The chatroom rules say authenticated writes assume the channel and member rows exist (apps/webapp/src/components/chatroom/CLAUDE.md:117). This defect breaks that assumption for comments.
A comment with an attachment fails sooner, at the upload. The upload policy needs a channel member row (packages/supabase/scripts/12-buckets.sql:179-189). A fix that creates the row during the load covers this too.
Issue #142 (closed 2024-12-19) reported a failed comment on a heading with no channel row. Then the RPC raised P0001, and the chat did not open. Today the RPC returns an empty row, so the chat opens and the send fails instead.
Evidence:
- A scratch harness mounted the real
useChannelMetadata in a DOM emulator, without React StrictMode. It used a fetch stub and an empty-room RPC reply. A plain open sent one POST /rest/v1/channels. After setCommentMessageMemory for the heading, the mount sent none, and channelInfo stayed null. When the first channel create request failed, a second mount sent no new one.
- A reviewer's StrictMode copy of the harness sent two channel create requests for one open, as dev does.
- On the local Supabase stack, as the
authenticated role in a transaction that was rolled back, a comment insert into messages for a missing channel failed with new row violates row-level security policy for table "messages". In a reviewer's second rolled-back test, the same insert succeeded after the user created the channel row.
- The write order in Problem item 2 is a code trace. The defect was not reproduced end to end in the running app.
Problem
A document comment fails to send when its heading has no channel row yet. The chat creates that row only on some opens. It decides from client state, not from the server.
On
14ab7f9c1. Webapp paths are underapps/webapp/src/. Paths that start withpackages/are from the repo root.useChannelMetadatacallsupsertChannelonly whenworkspaceSettings.channelshas no entry for the heading id (components/chatroom/hooks/useChannelMetadata.ts:29-44). That map is client state: mode memory, the member flag, andchannelInfo. It is not a record of channel rows.openHeadingChatroomwithintent: 'comment'callssetCommentMessageMemory(headingId, …)(services/openHeadingChatroom.ts:137-145). Then it schedules the pane (:153).setMemorycreates the entry when it is missing (stores/chat/workspaceSettingsStore.ts:93-102). So when the chat loads, the check passes and the create step is skipped. When the pane is already open on that heading, it only focuses the composer (services/openHeadingChatroom.ts:148-151). That chat has already run its load.stores/chat/bootstrapStore.ts:24-27). So after a create step that failed, a reopen of the same heading chat in the tab never runs it again.channel_infonull (packages/supabase/scripts/10-functions.sql:94-113).AccessControlshows the composer whenchannelInfois missing (components/chatroom/components/ChannelComposer/ChannelComposer.tsx:39).internal.can_read_channel(channel_id)(packages/supabase/scripts/13-RLS.sql:211-216). That check needs the channel row (packages/supabase/scripts/10-0-func-helpers.sql:143-162). The submit catch shows the error in a toast (components/chatroom/components/MessageComposer/hooks/useComposerSubmit.ts:172-179).Steps to reproduce
first commentand press Enter.public.channelshas no row with that heading id.Acceptance criteria
Agent Brief
Category: bug
Summary: The chat must create a heading's channel row when the server reports it missing, whatever the client store holds.
Current behavior:
useChannelMetadatatreats an entry inworkspaceSettings.channelsas proof that the channel row exists. A comment start writes that entry before the chat loads, so the create step is skipped. Every load writes an entry too, so a failed create step is never retried in the tab. The metadata RPC then returns no channel, the composer shows, and the comment insert fails on the messages insert policy.Desired behavior:
The chat creates the channel row whenever the row is missing, as the server reports it. A comment on a heading with no chat sends on the first try, like a message after a table of contents open does today.
Key interfaces:
useChannelMetadata()— the create gate, and the load that runs the metadata RPC.workspaceSettings.channels— per-channel client state. Its entries are not channel rows.setCommentMessageMemory()andsetMemory()— write an entry before any load.bootstrapChannel()— writes an entry on every load, andchannelInfoonly when the row exists.get_channel_aggregate_data— returnschannel_infonull when the channel row is missing.upsertChannel()— an upsert withonConflict: 'id'andignoreDuplicates: true. It returns no row when the row already exists.openHeadingChatroom()withintent: 'comment'.AccessControl— shows the composer whenchannelInfois missing.Out of scope
joinChannelreturning a policy error instead of throwing it. Run the chat join step when the profile loads after the chat opens #292 notes it as a separate defect.Notes
Use
channel_infofrom the metadata RPC as the missing-row signal, as #292 also advises. The RPC already runs on every open. After the create step, run the metadata RPC again, and store thechannelInfoit returns, soAccessControlgets the channel type. The create trigger already adds the creator as an admin member (packages/supabase/scripts/10-2-func-channels.sql:16-44). A join after that fails in the duplicate-member trigger (:345-371). So join only when the RPC still reports no membership. That happens when another user created the row first. Do not move mode memory out ofworkspaceSettings.channelsto fix this.The chatroom rules say authenticated writes assume the channel and member rows exist (
apps/webapp/src/components/chatroom/CLAUDE.md:117). This defect breaks that assumption for comments.A comment with an attachment fails sooner, at the upload. The upload policy needs a channel member row (
packages/supabase/scripts/12-buckets.sql:179-189). A fix that creates the row during the load covers this too.Issue #142 (closed 2024-12-19) reported a failed comment on a heading with no channel row. Then the RPC raised
P0001, and the chat did not open. Today the RPC returns an empty row, so the chat opens and the send fails instead.Evidence:
useChannelMetadatain a DOM emulator, without React StrictMode. It used a fetch stub and an empty-room RPC reply. A plain open sent onePOST /rest/v1/channels. AftersetCommentMessageMemoryfor the heading, the mount sent none, andchannelInfostayed null. When the first channel create request failed, a second mount sent no new one.authenticatedrole in a transaction that was rolled back, acommentinsert intomessagesfor a missing channel failed withnew row violates row-level security policy for table "messages". In a reviewer's second rolled-back test, the same insert succeeded after the user created the channel row.