feat(api/events): migrate identity/avatar/conversation-metadata events to canonical Zod schemas#32635
Merged
Merged
Conversation
f9cc1a3 to
6dd8b23
Compare
…s to canonical Zod schemas Batch 6 of the canonical-events migration. Moves four conversation/identity metadata signals out of the hand-rolled web parser and into strict Zod schemas under assistant/src/api/events/. Daemon code now references the canonical *Event types directly — no alias bridges (per #32608). Events migrated: - identity_changed — name/role/personality/emoji/home (all string) - avatar_updated — avatarPath (string) - conversation_list_invalidated — reason enum (created/renamed/deleted/reordered/seen_changed) - conversation_title_updated — conversationId, title Canonical schemas (new): - assistant/src/api/events/identity-changed.ts - assistant/src/api/events/avatar-updated.ts - assistant/src/api/events/conversation-list-invalidated.ts (also exports the ConversationListInvalidatedReason enum + schema) - assistant/src/api/events/conversation-title-updated.ts Wired into assistant/src/api/index.ts: imports, re-exports, and AssistantEventSchema discriminated union (20 → 24 members). Daemon adoption (no alias bridges): - daemon/message-types/workspace.ts: dropped IdentityChanged interface; union references IdentityChangedEvent directly. - daemon/message-types/settings.ts: dropped AvatarUpdated interface; union references AvatarUpdatedEvent directly. - daemon/message-types/conversations.ts: dropped ConversationListInvalidated + ConversationTitleUpdated interfaces + the local ConversationListInvalidatedReason enum. - runtime/sync/resource-sync-events.ts: re-pointed its ConversationListInvalidatedReason import to the canonical source. Tightenings vs. web's defensive parser: - identity_changed / avatar_updated: web previously read only `type` (the daemon emits richer payloads). Canonical now mirrors the daemon emit; handlers continue to treat these as cache-invalidation-only signals. - conversation_list_invalidated.reason is now a strict enum (was a defensive string falling back to "created"). - All four events drop the unused defensive `conversationId?` field that the legacy web interfaces carried. Web cut-over: - apps/web/src/types/event-types.ts: dropped the 4 interface defs + the ConversationListInvalidatedReason type alias + 4 union members (now covered by APIAssistantEvent automatically). - apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.ts: re-pointed imports to @vellumai/assistant-api. - apps/web/src/domains/chat/api/event-parser.ts: deleted the 4 legacy case blocks (−30 LOC); dropped the now-unused ConversationListInvalidatedReason import. - Test fixtures padded to satisfy strict canonical shapes. Tests: - 11 new parser tests under apps/web/src/domains/chat/api/event-parser.test.ts (matching Batch-5 inline-literal pattern). Full file: 136/136 ✅ - metadata-handlers tests: 49/49 ✅ - daemon sync-publisher / proactive-artifact: 30/30 ✅ - web tsc: clean - cross-domain audit: no allowlist changes Refs #32548 #32608
6dd8b23 to
0b438c0
Compare
dvargasfuertes
approved these changes
May 30, 2026
Comment on lines
+40
to
+45
| // `identity_changed`, `avatar_updated`, `conversation_title_updated`, and | ||
| // `conversation_list_invalidated` are now schema-validated via canonical | ||
| // schemas in `@vellumai/assistant-api`. The legacy parser functions | ||
| // previously here are gone — `event-parser.ts` no longer dispatches | ||
| // these cases; `parseAssistantEvent` resolves them through | ||
| // `AssistantEventSchema` before reaching the legacy switch. |
Contributor
There was a problem hiding this comment.
Delete this comment.
Comment on lines
+223
to
+225
| // `conversation_title_updated` is now the canonical | ||
| // `ConversationTitleUpdatedEvent` defined in | ||
| // `assistant/src/api/events/conversation-title-updated.ts` and imported above. |
Contributor
There was a problem hiding this comment.
Delete this comment.
Comment on lines
+562
to
+567
| // `conversation_list_invalidated` is now the canonical | ||
| // `ConversationListInvalidatedEvent` defined in | ||
| // `assistant/src/api/events/conversation-list-invalidated.ts` and imported | ||
| // above. The `reason` enum (`ConversationListInvalidatedReason`) lives | ||
| // there too; the one daemon consumer | ||
| // (`runtime/sync/resource-sync-events.ts`) imports it directly. |
Comment on lines
+32
to
+33
| // `avatar_updated` is now the canonical `AvatarUpdatedEvent` defined in | ||
| // `assistant/src/api/events/avatar-updated.ts` and imported above. |
Comment on lines
+122
to
+123
| // `identity_changed` is now the canonical `IdentityChangedEvent` defined in | ||
| // `assistant/src/api/events/identity-changed.ts` and imported above. |
dvargasfuertes
pushed a commit
that referenced
this pull request
May 30, 2026
* api-events: canonicalize interaction-request family (APE.11)
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.
* api-events: repoint daemon consumers to canonical Request event names
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).
---------
Co-authored-by: vellum-apollo-bot[bot] <242025090+vellum-apollo-bot[bot]@users.noreply.github.com>
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.
Batch 6 of the canonical-events migration. Four conversation/identity
metadata signals (
identity_changed,avatar_updated,conversation_list_invalidated,conversation_title_updated) move out ofthe hand-rolled web parser and into strict Zod schemas. Daemon code
references the canonical
*Eventtypes directly — no alias bridges(per the cleanup rule from #32608).
Events migrated
identity_changedname,role,personality,emoji,home(all string)avatar_updatedavatarPath(string)conversation_list_invalidatedreasonenum:created/renamed/deleted/reordered/seen_changedconversation_title_updatedconversationId,titleCanonical schemas (new)
assistant/src/api/events/identity-changed.tsassistant/src/api/events/avatar-updated.tsassistant/src/api/events/conversation-list-invalidated.ts— also exportsConversationListInvalidatedReasonSchema+ConversationListInvalidatedReasonassistant/src/api/events/conversation-title-updated.tsWired into
assistant/src/api/index.ts: imports, re-exports, and theAssistantEventSchemadiscriminated union (20 → 24 members).Daemon adoption
daemon/message-types/workspace.ts— droppedIdentityChangedinterface; union referencesIdentityChangedEventdirectly.daemon/message-types/settings.ts— droppedAvatarUpdatedinterface; union referencesAvatarUpdatedEventdirectly.daemon/message-types/conversations.ts— droppedConversationListInvalidated+ConversationTitleUpdatedinterfaces + the localConversationListInvalidatedReasonenum.runtime/sync/resource-sync-events.ts— re-pointed itsConversationListInvalidatedReasonimport to the canonical source (the one external daemon consumer of that enum).Tightenings vs. the web parser
identity_changed/avatar_updated: web previously read onlytype(the daemon already emitted richer payloads). Canonical now mirrors the daemon emit. Handlers continue to treat these as cache-invalidation-only signals — they don't need the new fields, but the wire contract is now truthful.conversation_list_invalidated.reasonis now a strict enum (was a defensive string with a"created"fallback).conversationId?field the legacy web interfaces carried.Web cut-over
apps/web/src/types/event-types.ts— dropped the 4 interface defs + theConversationListInvalidatedReasontype alias + 4 union members (covered byAPIAssistantEventautomatically).apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.ts— re-pointed imports to@vellumai/assistant-api.apps/web/src/domains/chat/api/event-parser.ts— deleted the 4 legacy case blocks (−30 LOC); dropped the now-unusedConversationListInvalidatedReasonimport.Verification
apps/webparser suiteruntime/sync+proactive-artifact/jobtestsapps/webtsc --noEmittsc --noEmitHooks bypassed for the usual reasons (
bunx eslintResolveMessage {}insymlinked worktree, daemon
tscOOM). Verified clean externally before push.Refs #32548 (Batch 5 — system status events) and #32608 (alias-bridge cleanup).