Repository navigation
feat(database,features,service): an Inbox item holds a thread of messages - #3469
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
14 tasks done
14 tasks done
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Oct 17, 2026
…ages
An inbox item now holds a thread beside its one answer. A member of the
item's workspace can add a message to it at any point, open or settled,
through POST /api/inbox/{id}/messages: Markdown words and the files a
message to an agent may carry. The message is kept in the new
inbox_message table, its files as inbox_attachment rows of the message
kind, and it moves the item's updated_at and nothing else of it, so the
status, the answer, and the revision stand. It reaches the agent through
the steer under its own lead, "Inbox message", naming the item's key and
title, which wakes an agent that stopped as a tab message does. A
hand-off that fails takes the message back off the thread, and a message
sent again under its key is written once.
Every item read carries its latest messages oldest first, each with its
author, words, files, and time, and a post under the item's key keeps
the thread and its files.
Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
… item settled A member's message under a key that already names another message, by another member or with other words or files, is refused, so the agent never hears what the thread does not show. Only the send that wrote a message takes it back when its hand-off fails. inbox_item gains settled_at, written when an item is answered or closed and cleared when it opens again, and the wire carries it as settledAt, so a message moving updatedAt no longer moves when an item settled. An item settled before the column reads its last change as settled until its first message, which keeps that instant first, so no backfill is needed. Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
main gained 20261017121543_coding_agent_start_branch, which this branch's migration did not commute with, so the folder is generated again from the rebased schema with the same hardened statements. Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
luke-agent
Bot
force-pushed
the
luke/inbox-thread-messages
branch
from
October 17, 2026 13:49
7625beb to
03a363e
Compare
Merged
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
The ask: Dean wants to be able to add a message to an Inbox item's thread at any point, not only the one answer an item takes today. This is the first of two PRs, the service half. The second PR adds the agent's
read_inboxthread and areply_inboxtool. A separate desktop worker builds the window on top of this one.inbox_messageholds the thread (packages/database/src/schemas/inbox-message.ts). Each row has:memberwith theiruser_id(set null when the account goes) oragent, the item's own;client_key, unique per item;created_at.inbox_attachmentwith a newmessagekind and amessage_idlink. They have the same bounds as an answer's file: the PDF bound, plus a name and a position. The kind check now pinsmessage_idto the message kind alone.inbox-wire.ts): an item read carriesmessages, oldest first, the latestMAX_MESSAGES_READ(50) of them. Each message is{ id, author: "member" | "agent", member?: { userId, name }, text, files?: [{ attachmentId, name, mediaType }], createdAt }. A file is fetched through the existing attachment path.messagesis absent where the thread is empty.POST /api/inbox/{id}/messagestakes{ text, attachments?, clientKey }, with words or files or both, and answers{ item }.updated_atand nothing else.status,answer, andrevisionstand.INBOX_MESSAGE_LEAD("Inbox message on "<title>" (key ):") with its files. This opens a turn on an agent that stopped, as a tab message does.clientKeyis written once and handed off again, and the steer's key makes it reach the agent once. An agent whose session has ended for good keeps the message.settled_at:inbox_itemgainssettled_at, written when an item is answered or closed and cleared when a post or a withdrawn answer reopens it. It reaches the wire as an optionalsettledAt, so the desktop no longer readsupdatedAt, which a message now moves, as the time an item settled.coalesce(settled_at, updated_at)for a settled item, and the first message on an old row copies itsupdated_atintosettled_atbefore moving it, so every row reads correctly.clientKeythat already names a different message (another member, other words, or other files) is refused asinvalid-request. Only the send that wrote a message takes it back when its hand-off fails.HostedCodingAgentClient.messageInbox(itemId, request)andcodingAgentInboxMessagedViewSchema, for the desktop's Gateway. Path builder:inboxMessagesPath.postInboxItem's attachment sweep now spares the message kind.updatedAtfor every open and latest-settled item, so a message wakes a held read at once, settled items included.max(updated_at)) at the hold's end, as an archive does today.inbox_itemrows, and a message updates that row, so a follow sees the change. That statement doesn't projectmessagesyet; it doesn't projectanswerFileseither. Whichever of the two lands second should addMESSAGES(ininbox-store.ts) to that projection.Migration (
20261017134832_inbox_message_thread)Online-safe and idempotent:
CREATE TABLE IF NOT EXISTSforinbox_message.inbox_attachment.message_idandinbox_item.settled_atcolumns with no default, each a catalog-only change.IF NOT EXISTSindexes. Theinbox_attachmentindex covers a few rows per item, their bytes stored out of line, and its column is null on every row.DROP CONSTRAINT IF EXISTS, thenADD, for each constraint.inbox_attachmentand the replaced kind check are addedNOT VALID, following20261015114324_message_sender. Every existing row already satisfies both, so nothing is scanned under the lock.JSON Schema goldens (
LUKE_UPDATE_FIXTURES=1)inbox-wire-inboxItemSchema.json: adds the optionalmessagesarray ofinboxMessage, and the optionalsettledAtcount.inbox-wire-inboxAnswerSchema.json,inbox-wire-inboxAnsweredSchema.json,coding-agent-view-codingAgentInboxAnswerViewSchema.json,coding-agent-view-codingAgentInboxAnsweredViewSchema.json: the samemessagesandsettledAtadditions, through the item they embed.inbox-wire-inboxMessageSchema.json(one message),inbox-wire-inboxMessageRequestSchema.json(the POST body),inbox-wire-inboxMessagedSchema.json(its answer),coding-agent-view-codingAgentInboxMessagedViewSchema.json(that answer or a failure, for the Gateway).Evidence
pnpm check:affectedgets killed by the sandbox's 8 GB limit (exit 137) when its tasks run in parallel, so I ran its parts one after another. All passed:pnpm knip,pnpm discarded-effect,pnpm oxlint, andbiome check.turbo run typecheck --concurrency=1over every dependent of@luke/databaseand@luke/features, desktop and host included.drizzle-kit check("Everything's fine"), anddb:generatereports no unmigrated schema.vitest run --project features --project database: 835 tests passed.vitest runovercoding-agents-app.test.ts,coder-host/,inbox/, and the web storage, functions, and services tests: 239 tests passed.Invariants
./scripts/check.shgreen. (CI) Locally, its parts ran one after another, as above. CI runs it whole.LUKE_UPDATE_FIXTURESnot run, or diff explained line by line). (CI) Explained above.AGENTS.md"Testing")inbox-store.test.ts): the starter and a teammate post on an open item and on an answered one; an outsider writes nothing; status, answer, and revision stay;updated_atand the plan read's cursor move; messages read oldest first with their author. A second test covers a message's files kept in order and served, a repost keeping the thread and its files, a repeat under the same key writing once and moving nothing, other words under that key refused as the key reused, and a withdrawn message going with its files. A third coverssettledAt.coding-agents-app.test.ts): a message on an answered item wakes an agent whose turn ended (statuscompleted→running) and reaches eve under "Inbox message" with its words and file, while the item stays answered at its revision. A repeat reaches eve once, and other words under the same key are refused without reaching eve; a teammate posts once they join and is refused before; no bearer and empty words are refused. A second test checks that a hand-off eve refuses leaves no message on the thread.coding-agent-client.test.ts): the path, the trimmed body, the answer, a refusal named, and nothing sent for an empty message.astype assertion outside the allowlisted files. (CI via anti-slop)Effect.runPromise/runSync/runForkoutside the runtime edges and the shimstools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-run-promise-outside-edges)setTimeout/setInterval/new Promise/AbortController/fs.watchoutside the filestools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-raw-async-primitives)pnpm discarded-effect)vitest run --reporter=json) +6: three store tests (the third coverssettledAtthrough an answer, a close, a row settled before the column, a reopen, and a message on an open item), two route tests, and one client test.AGENTS.md's "The permanent adaptors" with the reason it stays. Nothing is replaced.CLAUDE.md/AGENTS.mdsentence naming a changed part is edited; root pair stays byte-identical. None names these parts.PRIVACY.mdunchanged, or the change is a product decision called out in the PR body. Called out above.package.jsondeps match what the sources import by bare specifier;effectviacatalog:. (CI) Adds the./schemas/inbox-messageexport.@effect/platform-node,effect/sql) behind a barrel the renderer or a web function opens. (CI: existing renderernode:grep plus a new grep for those specifiers in renderer bundles)Notes
read_inboxandreply_inbox) stacks on it.messagesto the scope live statement, as noted under Summary.docs/WORKFLOW.mdstep 3's./scripts/run.sh --previewneeds the desktop half to draw the thread. Until then the route is covered by the tests above.Made with Luke
Summary by cubic
Lets any workspace member add follow-up messages to an inbox item's thread, beyond the single answer an item holds today.
inbox_messagetable stores each message with its author (member or the item's agent), Markdown text, files, and a per-itemclient_keyfor idempotent retries.POST /api/inbox/{id}/messagesaccepts{ text, attachments?, clientKey }from any member of the item's workspace, on open, answered, or closed items.updated_at, leaving status, answer, and revision untouched.inbox_itemnow stampssettled_atwhen an item is answered or closed and clears it when it opens, so a message no longer moves when the item settled.inbox_attachmentunder the newmessagekind.Migration
inbox_messageis created withCREATE TABLE IF NOT EXISTS.inbox_attachment.message_idandinbox_item.settled_atare added as nullable, no-default columns (catalog-only changes); no backfill, as an item settled before the column reads its last change as settled until its first message.NOT VALID, so no existing rows are scanned under lock.Written for commit 03a363e. Summary will update on new commits.