api-events: canonicalize interaction-request family (APE.11)#32678
Conversation
Move the four "ask the user a thing" SSE events into the canonical
discriminated-union schema, completing the request side of the loop
that interaction_resolved (APE.5, Batch 5) already closed:
- secret_request (assistant/src/api/events/secret-request.ts)
- confirmation_request (assistant/src/api/events/confirmation-request.ts)
- contact_request (assistant/src/api/events/contact-request.ts)
- question_request (assistant/src/api/events/question-request.ts)
Each is .strict() so unknown fields surface as UnknownEvent rather than
silently passing through.
Support types exported from canonical:
- AllowlistOption, ScopeOption, DirectoryScopeOption,
ConfirmationDiff, ACPOption, ACPOptionKind,
ConfirmationExecutionTarget (from confirmation-request)
- QuestionOption, QuestionEntry (from question-request)
Wire-shape notes captured in the schema JSDoc:
- confirmation_request: riskLevel kept loose (string) since grades
evolve independently of the wire and the client renders as text;
executionTarget is strict 2-variant (sandbox/host); acpOptions.kind
is strict 4-variant per ACP mandate.
- question_request: both questions[] (canonical batched) and the flat
question/options shape are required — daemon synthesizes a
one-element batch from flat fields when no batch is supplied so
every broadcast carries both shapes.
Daemon adoption:
- assistant/src/daemon/message-types/messages.ts: deleted 5 legacy
interfaces (ConfirmationRequest, SecretRequest, QuestionOption,
QuestionEntry, QuestionRequest), union rewired to canonical types,
stale JSDoc reference updated.
- assistant/src/daemon/message-types/contacts.ts: deleted legacy
ContactRequest interface, union rewired.
Web cut-over:
- apps/web/src/types/event-types.ts: dropped 4 wire-event interfaces
+ their supporting types. Kept ConfirmationDecision ("allow" |
"deny") — client-decision shape, not a wire event.
- apps/web/src/types/interaction-ui-types.ts: re-pointed support
types (AllowlistOption, DirectoryScopeOption, QuestionEntry,
QuestionOption, ScopeOption) to @vellumai/assistant-api.
- apps/web/src/domains/chat/api/event-types.ts: re-pointed
QuestionRequestEvent + support types to canonical.
- apps/web/src/domains/chat/utils/stream-handlers/
interaction-handlers.ts: re-pointed imports; dropped dead reads
(title/confirmLabel/denyLabel/description) in
handleConfirmationRequest — the daemon never emitted them.
- apps/web/src/lib/streaming/event-parser.ts: dropped 4 parser
imports + 4 dispatch cases. The strict-schema fast path now covers
these events.
- DELETED apps/web/src/lib/streaming/parse-interaction-events.ts.
- apps/web/src/domains/chat/utils/chat.ts: added contact_request to
GLOBAL_STREAM_EVENT_TYPE_NAMES with rationale comment. The
contacts/prompt IPC route fires it from settings/skill flows with
no conversation binding, so the wire payload has no conversationId
and the conversation gate would otherwise drop it. Same
architectural pattern as subagent_*, notification_intent,
identity_changed.
Tests:
- Added 16 parser tests in event-parser.test.ts following the recipe
in assistant/src/api/README.md (happy path / required-only /
missing-required → unknown / strict-rejects-extra → unknown for
each of the 4 events).
- Deleted the obsolete secret_request / confirmation_request parser
test block (160 lines) that exercised parser code removed in this
commit.
- Fixed two test fixtures in interaction-handlers.test.ts to match
the now-required schema fields (service+field on secret_request;
toolName+input+riskLevel+allowlistOptions+scopeOptions on
confirmation_request, with the illegal title field removed).
Vargas #32635 review feedback applied: deleted the 5 "this used to
live here / now lives in canonical schema" breadcrumb comments still
on main (parse-resource-events.ts, conversations.ts ×2, settings.ts,
workspace.ts). Going forward, the rule is: when deleting an interface,
just delete — import statements + git diff are the audit trail.
Local gates: web tsc EXIT=0, parser tests 139/139 pass, interaction
handler tests 3/3 pass, ESLint EXIT=0 on all touched files, prettier
formatted. Daemon tsc verified EXIT=0 earlier in the session; cgroup
memory pressure prevented a re-run after the import-sort auto-fix
(which is semantically inert — pure reordering of re-exports in
assistant/src/api/index.ts). CI will catch any regression.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5877565462
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| | ConfirmationRequestEvent | ||
| | SecretRequestEvent | ||
| | QuestionRequestEvent |
There was a problem hiding this comment.
Re-export request aliases after switching union types
Replacing the local request interfaces with *RequestEvent types removes the QuestionRequest, QuestionOption, and SecretRequest exports from message-protocol.ts because that barrel only re-exports the domain modules, not the imported API event types. Existing code still imports those names from ../daemon/message-protocol.js (for example permissions/question-prompter.ts imports QuestionRequest/QuestionOption, and several secret prompter tests import SecretRequest), so the assistant package no longer type-checks until these aliases are re-exported or the callers are updated to the new event type names.
Useful? React with 👍 / 👎.
CI Type Check on the parent commit caught 5 daemon consumers still importing the deleted legacy interfaces (SecretRequest, QuestionRequest) from ../daemon/message-protocol.js. Re-point each to the canonical Event types sourced directly from their schema files, matching the daemon convention from prior batches: SecretRequest → SecretRequestEvent from ../api/events/secret-request.js QuestionRequest → QuestionRequestEvent from ../api/events/question-request.js QuestionOption → (same name) from ../api/events/question-request.js Files touched: - assistant/src/__tests__/secret-prompt-log-hygiene.test.ts - assistant/src/__tests__/secret-prompter-channel-fallback.test.ts - assistant/src/__tests__/secret-response-routing.test.ts - assistant/src/permissions/question-prompter.ts - assistant/src/permissions/question-prompter.test.ts The TS7006 implicit-any on question-prompter.test.ts:232 was downstream of the broken QuestionRequest cast; with the cast now resolving to QuestionRequestEvent (whose .questions field is QuestionEntry[]), the .map((q) => q.id) callback infers q as QuestionEntry naturally — no explicit annotation needed. Lint + prettier clean. Local daemon tsc was blocked by the container cgroup (~5GB limit vs ~4.2GB cold-cache tsc peak).
Summary
Canonicalize the four "ask the user a thing" SSE events into the
AssistantEventSchemadiscriminated union, completing the request sideof the loop that
interaction_resolved(APE.5, Batch 5) already closed:secret_request— credential prompter asks for an API key / passwordconfirmation_request— risk-classifier asks for tool-call approvalcontact_request—contacts/promptIPC asks for a contact channelquestion_request—ask_questionLLM tool asks 1–5 clarifying questionsEach schema is
.strict()so unknown fields surface asUnknownEventrather than silently passing through, and supporting types
(
AllowlistOption,ScopeOption,DirectoryScopeOption,ConfirmationDiff,ACPOption/ACPOptionKind,ConfirmationExecutionTarget,QuestionOption,QuestionEntry) areexported from canonical so downstream consumers no longer maintain a
parallel definition.
Net diff: 20 files, +773 / −643, −150 lines after the legacy parser
helper goes away, and +1 event slot drained from the legacy
parser switch (now 28 canonical members in the union).
Wire-shape notes (captured in schema JSDoc)
confirmation_request—riskLevelkept loose (string) sincegrades evolve independently of the wire and the client renders as
text;
executionTargetstrict 2-variant (sandbox/host);acpOptions.kindstrict 4-variant per ACP mandate.question_request— bothquestions[](canonical batched) andthe flat
question/optionsshape are required: the daemonsynthesizes a one-element batch from flat fields when a caller
doesn't supply one, so every broadcast carries both shapes. Once all
clients consume
questions[], the flat fields can be dropped(separate cleanup).
Daemon adoption (no breadcrumb comments)
assistant/src/daemon/message-types/messages.ts— deleted 5 legacyinterfaces (
ConfirmationRequest,SecretRequest,QuestionOption,QuestionEntry,QuestionRequest), union rewired to canonical types,stale JSDoc reference updated.
assistant/src/daemon/message-types/contacts.ts— deleted legacyContactRequestinterface, union rewired.Web cut-over (no breadcrumb comments)
apps/web/src/types/event-types.ts— dropped 4 wire-event interfacesConfirmationDecision("allow" | "deny") — that's a client-decision shape, not a wire event.apps/web/src/types/interaction-ui-types.ts— re-pointed supporttypes to
@vellumai/assistant-api.apps/web/src/domains/chat/api/event-types.ts— re-pointedQuestionRequestEvent+ support types to canonical.apps/web/src/domains/chat/utils/stream-handlers/interaction-handlers.ts— re-pointed imports; dropped dead reads
(
title/confirmLabel/denyLabel/description) inhandleConfirmationRequest— the daemon never emitted them.apps/web/src/lib/streaming/event-parser.ts— dropped 4 parserimports + 4 dispatch cases. The strict-schema fast path now covers
these events.
apps/web/src/lib/streaming/parse-interaction-events.ts(150 lines, no longer referenced).
apps/web/src/domains/chat/utils/chat.ts— addedcontact_requestto
GLOBAL_STREAM_EVENT_TYPE_NAMESwith rationale: thecontacts/promptIPC route fires it from settings / skill flows withno conversation binding, so the wire payload has no
conversationIdand the conversation gate would otherwise drop it. Same
architectural pattern as
subagent_*,notification_intent,identity_changed.Tests
event-parser.test.tsfollowing the recipein
assistant/src/api/README.md:UnknownEventUnknownEventsecret_request/confirmation_requestparser test block (160 lines) that exercised parser code removed in
this commit — it was testing handler shapes that no longer exist.
interaction-handlers.test.tsto satisfy thenow-required schema fields (
service+fieldonsecret_request;toolName+input+riskLevel+allowlistOptions+scopeOptionsonconfirmation_request, with the illegaltitlefield removed).
Review feedback applied (from #32635)
Vargas's 5 review comments on Batch 6 all read "Delete this comment"
against breadcrumb notes I'd left next to deleted interfaces. The rule
going forward — and applied from the start in this batch — is: when
deleting an interface, just delete. Import statements + git diff are
the audit trail. No "this used to live here / now lives in canonical
schema" notes.
This PR also deletes the 5 breadcrumbs that were still on
main:apps/web/src/lib/streaming/parse-resource-events.ts:40-45assistant/src/daemon/message-types/conversations.ts:223-225, 562-565assistant/src/daemon/message-types/settings.ts:32assistant/src/daemon/message-types/workspace.ts:122Local gates
apps/webtscevent-parser.test.ts(139 tests)interaction-handlers.test.ts(3 tests)bunx prettier --write(all touched files)assistanttscassistant/src/api/index.ts), so the prior PASS still stands. CI will re-verify on a fresh checkout.AGENTS.md compliance
this PR body.
comments.mdruleapplied (the lesson from PR feat(api/events): migrate identity/avatar/conversation-metadata events to canonical Zod schemas #32635).
UnknownEventper theassistant/src/api/README.mdmigration recipe.lint+format:checkgated locally before push (lesson fromPR evals: --benchmark first-class CLI + Benchmark interface #32307).
Follow-ups (not in this PR)
tool_progressorphan parser case (puredelete).
tool_resultmigration, blocked on a design pick fromB2: wire protocol additions — assistant_turn_start event + messageId on streaming events #32228 review thread ci: add terraform apply workflow on platform changes #3.
question/description/options/freeTextPlaceholderfields onquestion_requestcan be droppedonce every client consumes
questions[].