feat(features,service,dev): the agent reads and replies on its Inbox threads
Builds on #3469, which has merged; this PR now targets main.
Summary
This is the agent half of Dean's request: a message can be added to an Inbox item's thread at any point. #3469 is the service half, and a separate worker builds the desktop half.
read_inboxshows each item's thread. Every inbox call answers with each item'sthread, oldest first. Each entry is{ from, text, files? }:fromis a member by name, "a former member" when their account is gone, or "you" for the agent itself.fileslists the files by name. A thread shows on settled items too, since a member can write on those.reply_inboxis a new tool inINBOX_TOOLS, with its eve fileapps/dev/tools/reply_inbox.tsmodelled onpost_to_inbox.ts.- Input:
{ key, text, images?: [{ path }] }. It needs words, pictures, or both.textis Markdown of at most 8,000 characters, and there are at most 4 pictures. - Pictures are named by sandbox path, as an image block's are. They are read and sniffed through
readSandboxImagewith the reply's own refusal wording, held to the image bound, and kept as message files named by the file's base name. - What it writes: an agent-authored message on one of the agent's own items, open or settled. Status, answer, revision, and
settledAtstay as they are, andupdatedAtmoves. - What it refuses: a key that names none of the agent's items, a reply carrying neither words nor pictures, and a file that is not a picture. A refused reply writes nothing.
- Input:
- When to use it. The tool description says so, and the coder instructions now say it too: use it to answer a member's message, which arrives as a developer's message starting "Inbox message" and naming the key, or to follow up on an item after its answer. When the item itself changes, post it again with
post_to_inboxunder the same key. - Registration:
AGENT_TOOL.REPLY_INBOX(coder only),CODER_TOOL.REPLY_INBOX, its context estimate, and the host's inbox door, which hands a reply the sandbox as it does a post. - Sandbox and membership: a reply carrying pictures counts as opening the sandbox (
opensSandbox), so the turn's end records the branch and stops the sandbox. A reply is written only where the agent's starter is still a member of the item's workspace. - Store:
replyInboxItem(agent, key, text, pictures)finds the agent's own item by key and writes through the samewriteInboxMessageas a member's message. That function now takes files as{ name, contentType, bytes }.
The desktop's tool-call card has no case for reply_inbox yet, so it draws the generic card. Drawing it as an inbox card is left to the desktop half.
Evidence
- Platform-independent checks:
pnpm check:affectedis killed by the sandbox's 8 GB limit 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/features.
vitest run packages/service/src/agents/coder-host packages/service/src/agents/agent-runtime packages/service/src/agents/link-examples.test.ts: 25 files, 135 tests passed.hosted-wire-schemas.test.ts: 359 tests passed.
Invariants
-
./scripts/check.shgreen. (CI) Locally, its parts ran one after another, as above. - JSON Schema goldens unchanged (
LUKE_UPDATE_FIXTURESnot run, or diff explained line by line). (CI) One golden is new:inbox-wire-inboxReplyInputSchema.json, the tool's input{ key, text, images? }.packages/service/fixtures/coder-instructions/chat-off.jsongains one sentence in each of its four prompts, thereply_inboxguidance above; I checked that this sentence is the only difference. - Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (manual; root
AGENTS.md"Testing") Two tests incoder-inbox.test.ts:read_inboxreturns the thread in order: a member's message under their name with its file named, then the agent's own reply as "you".reply_inboxwrites the agent's message with its picture, which is read from the sandbox, kept, and served. Status, answer, revision, andsettledAtstay, andupdatedAtmoves. It refuses another agent's key, an unknown key, empty words, and a file that is not a picture, and none of them writes anything.
- Gateway envelope goldens unchanged. (CI)
- No new
astype assertion outside the allowlisted files. (CI via anti-slop) The tool file'sas WireBoundaryInputis the same one every eve tool file carries. - No
Effect.runPromise/runSync/runForkoutside the runtime edges and the shimstools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-run-promise-outside-edges) - No new
setTimeout/setInterval/new Promise/AbortController/fs.watchoutside the filestools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-raw-async-primitives) - No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via
pnpm discarded-effect) - Test count equals the pre-PR count or the delta is listed. (manual:
vitest run --reporter=json) +4: the two above, a reply refused once its starter leaves the item's workspace, and the sandbox classifier counting a reply with pictures. - Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root
AGENTS.md's "The permanent adaptors" with the reason it stays. Nothing is replaced. - Every
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. Unchanged; #3469 already describes a thread's messages. - Package
package.jsondeps match what the sources import by bare specifier;effectviacatalog:. (CI) - Barrel/door rule: no Node-reaching Effect module (
@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
- Blockers or follow-up verification: none beyond #3469 merging first. Once it does, I'll retarget this PR to
main.
Made with Luke
Summary by cubic
The coding agent can now read and reply on its Inbox threads. read_inbox answers with each item's thread, oldest first, naming each writer ("you" for the agent, the member by name, or "a former member") and the files by name, on settled items too. The new reply_inbox tool writes the agent's message on one of its own items by key, with Markdown and up to 4 pictures read from the sandbox; it leaves status, answer, revision, and settledAt as they stand, moves updatedAt, and refuses unknown keys, replies with neither words nor pictures, and files that are not pictures. The coder instructions now say when to use it: to answer a member's "Inbox message" or to follow up after an answer, and to post the item again under the same key only when what it asks changes.
- A reply with pictures marks the sandbox opened, so the session records the branch and stops the sandbox as after any other sandbox read; a reply with words alone opens none.
- A reply is refused where the agent's starter has left the item's workspace; the agent's own existing posts already behave this way.