Add a branded AgentMessageId assigned to each accepted send message and
returned from send/followup/steer/inject (was void). Rename the inbox
event payload InboxItemInfo to AgentMessage, carrying that id so a caller
can correlate a queued item with its enqueue/dequeue/discard events.
The composer seat lives in a fixed-height conversation column with
overflow hidden, so a long question batch pushed the footer actions out
of reach (review r3635427112). The card now flexes with a viewport-
relative max-height, the option list is the scrollable region (ChatView
list pattern: min-height 0 + overflow-y auto), and header/footer are
flex-shrink 0 so progress, navigation, skip, submit, and cancel stay
reachable.
Verified in headless chromium against this sheet (900x600 viewport,
30-option batch): card capped at 360px (60vh), the option list scrolls
(scrollHeight > clientHeight), and the footer submit/skip buttons stay
inside the viewport. jsdom cannot assert layout; the playwright smoke
follow-up tracks composer scrolling with the existing debt.
AskUserQuestionItem.detail is part of the user-interaction seam contract
but the web frame schema dropped it and the composer never rendered it
(review r3635427108). askUserQuestionItemSchema now forwards detail, the
composer renders it under the title in the description text style, and
the fixture's multi-select question carries one.
ask() already fails EMPTY_QUESTIONS before a request exists, so a
question/requested frame with zero items is host breakage; muxFrameSchema
now refuses it instead of letting an undefined first question reach the
composer (review r3635427102).
Two hardenings on the chain outlet branch:
A throwing chain selector runs before its entry's SlotErrorBoundary
exists, so uncontained it blacked out the whole owner region and skipped
the remaining chain. It now degrades to a decline: reported via
console.error with the registrant identity, later entries still tried,
all-null/all-throw passes land on the owner fallback.
The elected entry's boundary is now keyed by entry identity: an unkeyed
boundary that failed on entry A survived a re-election and kept a
healthy entry B blacked out until the outlet unmounted. The key remounts
the boundary fresh whenever the election changes.
ClientContext types ctx.slots as always present (inject-declared service);
the unnecessary-condition lint rule rejects the dead guard and its
fail-loud test premise. Load-order failure still surfaces loud through
the undeclared-slot registration path, covered by the remaining case.
The FIFO-conservation invariant fired on the loop-authored continuation
reason path: a continue-with-reason decision entered the steering FIFO
without an agent/inbox/enqueue, so its later dequeue/discard had no
matching enqueue. Emit the enqueue for that steer too, add a regression
test that mounts the invariant over a continue-with-reason turn and a
cancel, and hoist the duplicated inboxInfo helper into inbox.ts.
Found by fresh-eye review.