Q142: a conversation is read only up to the message the reader actually saw

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 23:18:06 -04:00
1 parent 87c297dd45
commit b18eb9ad14
11 files changed
+167 -55

No files matched your search

+8 -11
View File
@@ -220,22 +220,19 @@ export class ChatService {
const employee = this.db.prepare("select id from employees where id = ? and org_id = ?")
.get(input.employeeId, input.orgId) as { id: string } | undefined;
if (!employee) throw new Error("Employee was not found.");
// Read state is per-employee bookkeeping, not access control: the board
// observes every conversation in its org (agent↔agent directs included),
// so opening one clears its unread count even without membership.
const latest = this.db.prepare(`
select id from chat_messages
where space_id = ? and org_id = ?
order by created_at desc
limit 1
`).get(input.spaceId, input.orgId) as { id: string } | undefined;
const now = new Date().toISOString();
// Read state is per-employee bookkeeping, not access control: the board reads every conversation in its org.
const seen = this.db.prepare("select id, created_at from chat_messages where id = ? and space_id = ? and org_id = ?")
.get(input.upToMessageId, input.spaceId, input.orgId) as { id: string; created_at: string } | undefined;
if (!seen) throw new Error("The message read is not in this conversation.");
// Read up to what was seen, never past it; messages order by time then insertion, and a late mark never moves back.
this.db.prepare(`
insert into chat_read_state(org_id, employee_id, space_id, last_read_message_id, last_read_at)
values (?, ?, ?, ?, ?)
on conflict(employee_id, space_id)
do update set last_read_message_id = excluded.last_read_message_id, last_read_at = excluded.last_read_at
`).run(input.orgId, input.employeeId, input.spaceId, latest?.id ?? null, now);
where (excluded.last_read_at, (select rowid from chat_messages where id = excluded.last_read_message_id))
> (chat_read_state.last_read_at, coalesce((select rowid from chat_messages where id = chat_read_state.last_read_message_id), -1))
`).run(input.orgId, input.employeeId, input.spaceId, seen.id, seen.created_at);
}
editMessage(input: EditMessageInput): void {
+2 -2
View File
@@ -96,11 +96,11 @@ export class SnapshotService {
approvals: list(this.db, "approvals", "created_at desc"),
chatSpaces: list(this.db, "chat_spaces", "created_at asc"),
chatMemberships: list(this.db, "chat_memberships", "space_id asc, principal_type asc, principal_id asc"),
chatMessages: recentList<AppSnapshot["chatMessages"][number]>(this.db, "chat_messages", "created_at desc", 500).reverse(),
chatMessages: recentList<AppSnapshot["chatMessages"][number]>(this.db, "chat_messages", "created_at desc, rowid desc", 500).reverse(),
// Every thread a loaded message belongs to, plus the most recently active ones, so no shown message loses its thread.
chatThreads: all<Row>(this.db.prepare(`
select * from chat_threads
where id in (select thread_id from (select thread_id from chat_messages order by created_at desc limit 500))
where id in (select thread_id from (select thread_id from chat_messages order by created_at desc, rowid desc limit 500))
or id in (select id from chat_threads order by updated_at desc limit 200)
order by updated_at desc
`)).map((row) => camel(row) as unknown as AppSnapshot["chatThreads"][number]),