Repository navigation
feat(service,features,database): keep each member's read state on Inbox items - #3618
Merged
Merged
Conversation
…ox items An inbox item now carries `unread` for the member reading it, kept per member and item in `inbox_item_read`. An item is unread while it changed after the last change the member read through, or while they marked it unread. A member's own answer or message marks it read for them in the same write. `PUT`/`DELETE /api/inbox/:id/read` mark one item read or unread, and `PUT /api/inbox/read` marks every item the member's Inbox lists read, optionally in one workspace. The held reads' cursors digest the reader's unread state, so a marker moving wakes that member's reads. Items that stood before this ships read as read: the migration adds `inbox_item.read_through` with a `now()` default kept in the catalog, which stamps every standing row without rewriting it, and drops the default, so items posted after it start unread. No backfill. Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
A read selects the item's last change before it takes the marker's lock, so a member's own answer recorded meanwhile could be overwritten by the older change, reading their answer as unread. Both read upserts now keep the later of the two and write only where the marker moves, so mark all counts only what it moved. Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
14 tasks done
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the service and wire half of the Inbox list-row redesign (
docs/proposals/inbox-threads.md§3–§4). Every Inbox item now carries the reader'sunreadflag. A member can mark one item read or unread, or mark every item read.inbox_item_read: one marker per member per item, holdingread_through(the item'supdated_atthe member has read up to) andmarked_unread. There is a unique index on(item_id, user_id).unreadForinpackages/service/src/inbox/inbox-store.ts). An item is unread for a member when they marked it unread, or when itsupdated_atis later than the later of theirread_throughand the item's ownread_through.updated_atmakes the item unread again for everyone who had read it. That covers a post or repost under its key, a close, another member's answer or message, the agent'sreply_inbox, and an answer being withdrawn.inbox_item.read_through timestamptz DEFAULT now()and then drops the default in the same transaction.now()is stable, so Postgres works it out once and keeps it in the catalog as the value for every existing row. No row is rewritten, and this is not a backfill.NULLand start unread.db:generate. The snapshot is unchanged (nullable column, no default), anddrizzle-kit checkpasses.inboxItemSchemagainsunread?: boolean. It is used by the plan inbox, the agent inbox, and the item returned by an answer or a message.accountInboxEntrySchemaandaccountTasklessEntrySchemagainunread?: boolean.optionalKeyso that a desktop talking to an older service reads a missing value as "read", the same convention asstartedBy.apps/web/server/routes/inbox/read.ts):PUT /api/inbox/:id/readmarks one item read. It answers{ id, unread: false }and is idempotent: when the marker already stands, it writes nothing (a conflictsetWhereskips the update). A read only ever moves a marker forward (greatestof the existing and newread_through), so a read that raced the member's own answer can't make that answer read as unread.DELETE /api/inbox/:id/readmarks one item unread. It answers{ id, unread: true }.PUT /api/inbox/readwith body{ workspaceId?: string }marks read every item the member's Inbox lists: items on tasks that aren't archived, and items of agents with no task. Settled items are included. It covers one workspace, or all of them when no workspace is given, and answers{ marked: number }, the count that was unread. It clears the dots only; open items still wait.not-found./api/inbox/readrewrite comes before/api/inbox/([^/]+). To allow that,SegmentRewrite.queryis now optional.HostedCodingAgentClientmethods:markInboxItemRead(itemId)andmarkInboxItemUnread(itemId)return aCodingAgentInboxReadStateView.markInboxRead({ workspaceId? })returns aCodingAgentInboxReadAllView.unread, andcursorOfdigests it when present. A member's own marker changing wakes their plan, agent, and account held reads, and leaves teammates' reads alone.inbox_item_readfor the reader as moving their inbox.Evidence
pnpm check:affectedran. The repository contract checks,check:migrations(drizzle-kit check), oxlint and biome, and knip all passed, along with the ui and service vitest suites. Some parts failed for sandbox reasons only, so I re-ran them on their own and they passed:@luke/servicetypecheck was OOM-killed (exit 137) anddiscarded-effectran out of heap. Run one at a time, the service, web, host, and desktop typechecks all passed, andpnpm discarded-effectreported no discarded Effect in 2279 files.desktopapp-menu.test.tsfails because there is no Electron binary in the sandbox. It is unrelated to this change.vitest --project web --project features --project database --project hostpassed: 174 files.inbox-store.test.ts› "an inbox item's read state":apps/web/tests/inbox-item-read-migration.test.ts: this runs the real migration over an item that already exists. That item reads as read for both members, an item posted afterwards reads unread, no markers are written, and running the migration again changes nothing.coding-agents-app.test.ts: covers the route (PUT/DELETE, mark all, 404 for an outsider, 405, 400).coding-agent-client.test.ts: covers the three client methods, their paths and bodies, and their refusals.Invariants
./scripts/check.shgreen. (CI)LUKE_UPDATE_FIXTURES=1. Each change:inbox-wire-inboxItemSchema, plus the schemas that embed it (inboxAnswerSchema,inboxAnsweredSchema,inboxMessagedSchema,codingAgentInboxAnswerViewSchema,codingAgentInboxAnsweredViewSchema,codingAgentInboxMessagedViewSchema): add the optional propertyunread: boolean.account-inbox-wire-accountInboxEntrySchemaandaccountTasklessEntrySchema, plus the schemas that embed them (accountInboxAnswerSchema,codingAgentAccountInboxAnswerViewSchema, 6 lines each): add the optional propertyunread: boolean.inboxReadStateSchema({ id, unread }),inboxReadAllRequestSchema({ workspaceId? }),inboxReadAllSchema({ marked }),codingAgentInboxReadStateViewSchema, andcodingAgentInboxReadAllViewSchema(each answer, or a failure).astype assertion outside the allowlisted files.Effect.runPromise/runSync/runForkoutside the runtime edges.setTimeout/setInterval/new Promise/AbortController/fs.watch.CLAUDE.md/AGENTS.mdsentence names a changed part.PRIVACY.mdunchanged. A read marker is the member's own state on their own workspace's items.@luke/database/schemas/inbox-item-readexport was added.Notes
DEFAULTonADD COLUMN.unreadand call the three client methods. The host acts and IPC are left to that work.Made with Luke
Summary by cubic
Keeps each member's own read state on Inbox items so the Inbox lists show what is unread for the reader. Every Inbox item now carries
unread, and a member can mark one item read or unread, or mark every item their Inbox lists read.Storage and rule
inbox_item_readtable holds one marker per member per item; an item is unread when marked unread or changed after the later of the member's and the item's read-through points.inbox_item.read_throughwith a catalog-onlynow()default and drops it in the same transaction, so no rows are rewritten and items posted afterwards start unread.Wire and routes
inboxItemSchemaand the account inbox entry schemas gain optionalunread; a missing value reads as read, the same convention asstartedBy.PUT/DELETE /api/inbox/:id/read(idempotent for an already-standing marker) andPUT /api/inbox/read(optionally scoped to one workspace) answer the item's read state and the count marked; the client gets three matching methods.Written for commit 986b702. Summary will update on new commits.