fix(types.events): CustomEvent extends BaseEvent so custom-event routing survives the wire - #52
Open
laithalsaadoon wants to merge 1 commit into
Conversation
…ing survives the wire CustomEvent declares the routing fields every other event declares (id, timestamp, thread_id, thread_name, message_id), so EVENT_ADAPTER keeps thread_id in the wire dict instead of sweeping it into payload and dropping it. Over a CoordinatorClient a custom event now matches a subscription filtered by thread_id, comes back from get_events(thread_id) with that id set, and is accepted by the remote coordinator's append_event; unrouted custom events were previously the only events a filtered subscriber could never see. Event becomes Annotated[SystemEvent | CustomEvent, Field(union_mode="left_to_right")]. Smart mode tie-breaks two matching union members by the number of fields set, and CustomEvent now declares every routing field, so it would outscore a fieldless concrete variant such as StartedEvent and swallow a built-in kind. Left-to-right keeps the discriminated SystemEvent union as the first attempt and CustomEvent as the fallback for unknown kinds only; the allowlist entry mirrors types.events.SystemEvent's. The serializer emits the declared fields alongside the flattened payload and re-nests payload entries whose keys shadow a declared field under a "payload" key, so a payload entry named id or thread_id (codex_plan emits one) cannot overwrite the event's own routing on a round trip. Breaking: a top-level key naming a routing field binds to that field, so CustomEvent(kind=..., thread_id=x) routes rather than filling payload; and CustomEvent inherits BaseEvent's frozen config, so a caller that mutated an instance must use model_copy(update=...) instead. An old peer parses a new frame unchanged: its CustomEvent has no routing fields declared, so it sweeps id/timestamp/thread_id/thread_name/message_id into payload exactly as it sweeps any other unknown key. A new peer parses an old frame with the routing fields at their defaults (a fresh id, now() timestamp, thread_id None), which the runtime gate then stamps. CoordinatorClient.append_event stays fire-and-forget and records each failure on the public append_errors property (last 32, oldest first) at ERROR, so a caller can observe a rejected append instead of only finding it in a log.
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.
Summary
CustomEventnow extendsBaseEvent, so it declares the same routing fields every other event declares (id,timestamp,thread_id,thread_name,message_id) and they survive serialization. Before this changeLocalWorker._route_eventstampedthread_idonto an attribute the model did not declare, which worked in one process but was dropped byEVENT_ADAPTERon the way to the wire. Over aCoordinatorClient, every custom event arrived withthread_id=None: filtered subscriptions (coord.on(cb, thread_id=...)) never fired for custom events,get_events(thread_id)returned them unrouted, and a client-sideappend_event(CustomEvent(...))was rejected by the remote coordinator and only logged.Closes #48.
What changed
types/events.py:CustomEvent(BaseEvent)withkindandpayload;EventbecomesAnnotated[SystemEvent | CustomEvent, Field(union_mode="left_to_right")]. The discriminatedSystemEventunion is tried first andCustomEventis the fallback for unknown kinds only. Smart-mode union resolution would otherwise tie-break by the number of fields set, and aCustomEventthat now declares every routing field would outscore a fieldless concrete variant such asStartedEvent.id,thread_id, ...) is re-nested under apayloadkey so it cannot overwrite the event's own routing on a round trip. (codex_planemits a payload entry namedid.)network/client.py:CoordinatorClient.append_eventstays fire-and-forget and records each failure on a new publicappend_errorsproperty (last 32, oldest first) at ERROR level, so a caller can observe a rejected append instead of only finding it in a log.ai_thread/summarization.py,cli/commands.py,runtime/usage.py,runtime/worker.py: read the now-declared fields directly instead ofgetattrfallbacks.spec/stubs mirror the new shapes;stubtest-allowlist.txtgains theEventunion entry, mirroring the existingSystemEventone.Behavior changes
CustomEvent(kind=..., thread_id=x)routes rather than fillingpayload.CustomEventinheritsBaseEvent's frozen config. A caller that mutated an instance in place must usemodel_copy(update=...).Wire compatibility
id/timestamp/thread_id/thread_name/message_idas unknown keys and sweeps them intopayload, exactly as it sweeps any other unknown key today.id, anow()timestamp,thread_id=None), which the runtime gate then stamps as before.Tests
tests/test_events_custom_wire.py(new) covers three layers: the adapter keeps routing on a stamped custom event and re-nests shadowing payload keys; the runtime gate stamps the declared field; a realCoordinatorEndpointplusCoordinatorClientagree on routing (filtered subscription fires,get_eventsreturns the id, client-sideappend_eventlands, a rejected append shows up onappend_errors).tests/test_pure_functions.pyandtests/test_reconstruct_roundtrip.pygain the union-order and round-trip cases.Gate
Run on this branch (
23c9ce39, base1125f0c4), mcp 2.1.1: