feat(features): inbox wire v5 adds asks and answers on messages
Step 2 of 7 of the inbox threads rollout: adds the vocabulary for an ask on a thread message and the answer to it, in packages/features. All of it is additive. Nothing a desktop or the service reads today changes shape.
What changed
Chat's ask shapes are now chat's own (chat-wire.ts). Chat used to re-export inboxResponseSchema, inboxReadResponseSchema, inboxReplySchema and inboxReadBlockSchema as its own. It now declares its own copies of the v4 versions: chatResponseSchema, chatReplySchema, chatReadBlockSchema, chatReadResponseSchema, a chat-only unknown-type placeholder, knownResponse/knownBlock, and the ChatResponse/ChatChoice/ChatForm/ChatReply/ChatReadBlock/ChatReadResponse types. Later steps of this rollout therefore can't move chat's shapes. Every chat-wire-* golden is byte-for-byte the same. The leaf pieces chat still shares with the inbox (option, field, form values, stored block) are the ones v5 reuses unchanged.
The ask (inbox-wire.ts):
INBOX_ASK(decide,do,answer) andinboxAskSchema:decide: 2–6inboxOptionSchemaoptions,multiple?,recommended?.do:steps, each Markdown of at mostMAX_STEP_CHARS(600), at mostMAX_STEPS(8).answer:fields?, 1–8 of the existinginboxFieldSchema. No new recommended values on fields.
inboxReadAskSchema: the ask as a message read carries it, withstatusfromINBOX_ASK_STATUS(open,answered,replaced,withdrawn) andwithdrawnReason?. An ask type this build doesn't know reads as{ type, status }alone, the same wayunknownTypeSchemahandles blocks and responses.unknownTypeSchemais now built by a smallunknownTypeOf(known)so asks have their own set of known types.
A do ask may have 0 steps. A v4 to-do named no steps: the item's blocks say what to do. So task maps to { type: "do", steps: [] } without inventing a step, and Mark done on it ticks nothing, since "every step ticked" holds for zero steps. Step 6's tool input can still ask the agent for at least one step if we want that.
The answer. inboxAnswersSchema holds ask (the asking message's id), reply (from INBOX_REPLY), choices?, values?, and steps? (which steps were ticked, as integer positions). The answerer's words and files belong to the message itself. inboxAnswersRefusal(ask, answers, words, files?) is the pure refusal. It reuses choiceRefusal/formRefusal, now typed on the options/multiple and fields they read, plus a shared ownWordsRefusal:
decidetakes a pick of offered options (chose), or words or files (answered).dotakes Mark done only with every step ticked (done), or Couldn't do it (not-done) with any ticked. It refuses a step ticked twice or a tick that names no step.answerwith fields takes filledsubmittedvalues. Without fields it takesansweredwith words or files.
Messages carry them. inboxMessageSchema gains optional ask and answers. The body stays text on the wire, because older readers require it.
Mappings, as pure functions:
inboxAskResponse: an ask to the nearest v4 response.decide→choice,do→task,answerwith fields →form, without →text.inboxResponseAsk: a stored v4 response to an ask.choice→decide,approval→decidewithINBOX_APPROVAL_OPTIONApprove/Decline,task→dowith no steps,form/text→answer,none→ no ask.inboxReplyAnswers: an older desktop'sInboxReplyto the answer of the open ask. Approved/Declined become a pick of Approve/Decline. Done ticks every step, because an older desktop shows none to tick. The note becomes the message's words. Acknowledged answers no ask.
INBOX_WIRE_VERSION = 5, with the doc comment and the module header saying what v5 adds.
Golden diff (LUKE_UPDATE_FIXTURES=1 through hosted-wire-schemas.test.ts)
- New:
inbox-wire-inboxAskSchema.json,inbox-wire-inboxReadAskSchema.json,inbox-wire-inboxAnswersSchema.json. - Grown by the same 274 added lines, none removed: the two new optional message properties
askandanswers, in every schema that embedsinboxMessageSchema. That isinbox-wire-inboxMessageSchema,-inboxItemSchema,-inboxAnswerSchema,-inboxAnsweredSchema,-inboxMessagedSchema, andcoding-agent-view-codingAgentInbox{Answer,Answered,Messaged}ViewSchema. - Unchanged: every
chat-wire-*golden.
Tests
inbox-asks.test.ts tests through the module's public exports:
- decoding a message with an ask in each status, with an answer, and with neither;
- a known ask that breaks its own shape being refused;
- an unknown ask type reading as the placeholder;
- an older reader decoding a v5 item. The test writes out the v4 message struct by hand and reads through
readServiceAnswer: the item reads whole and its messages drop the new keys; - every mapping in both directions;
- every refusal message.
Held back for later steps
knownAsk(a predicate on a read ask) and exportedInboxAsk/InboxAnswers/InboxReadAsktypes: nothing here uses them yet, and knip would refuse them.- The new
post_to_inboxinput schema andwithdraw_ask(step 6). - Tables (step 3), the store (step 4), answering by ask (step 5), the panel (step 7).
Checks
pnpm check:affected: lint, knip, discarded-effect, tests and build pass. In the 8 GB sandbox the combined run gets killed for memory: parallel tsc exits 137 and the web build hits the heap limit. So I ran each dependent workspace's typecheck one at a time (service, database, panel, ui, host, web, dev, luke, desktop, all clean) and the web build with a larger heap (clean).
Made with Luke