feat(chat): core lifecycle events — message updated/deleted, installed/uninstalled, app context changed (#196) - #272
Conversation
…d/uninstalled, app context changed (#196) Ports the core halves of vercel/chat 4ac04551 (#788), 2e2426d1 (#914) and 1721fa01 (#684): on_message_updated/on_message_deleted with process_message_updated/process_message_deleted (no routing, dedupe or locks; bot self-edits skipped), on_installed/on_uninstalled with process_installed/process_uninstalled (kept off the ChatInstance Protocol, optional upstream), on_app_context_changed/process_app_context_changed, the AppContextEntity dataclasses, AppHomeOpenedEvent entities/tab, Slack set_suggested_prompts optional thread_ts, and chat_sdk.testing.create_mock_chat_instance. Every new dispatch path runs inside the #195 active conversation. Part of #184
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 33 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (16)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… suggested prompts via api_call (#196) Review follow-ups: installation handlers now reach wait_until through the shielded _hand_to_wait_until wrapper (a host cancelling it no longer cancels the handlers), and set_suggested_prompts calls api_call so a threadless request works on slack-sdk < 3.43.0, whose generated helper requires thread_ts.
…nd and mock-chat-instance tests, app-context-matcher note (#196)
# Conflicts: # docs/UPSTREAM_SYNC.md # scripts/fidelity_target.json # src/chat_sdk/__init__.py # src/chat_sdk/chat.py
…es ChatInstance after #197
… in update dispatch
…ms flaked under CI 3.12 coverage)
|
Merge gate: CI green (test 3.12, test 3.13, Lint & Type Check, CodeQL/Analyze python+actions) on aec9c4a; local Codex review (gpt-6-astra, xhigh, --base origin/main) on aec9c4a: "No actionable regressions found against the specified merge base. Validation passed: 6,709 tests, Ruff checks, Pyrefly, and strict test fidelity verification"; 4 astra rounds after merging main (#197): r1 [P2] create_mock_chat_instance lacked |
Summary
Ports the core half of three upstream lifecycle features: registration, dispatch and types. The Slack and Teams emitters are separate issues (#211, #214, #217), so no platform emits these events yet and the change is additive.
chat.on_message_updated(handler)is called ashandler(thread, message, previous_message).Chat.process_message_updated(adapter, thread_id, message, previous_message=None, options=None)accepts each message as aMessageor an async factory. It binds the adapter, skipsauthor.is_me, builds the Thread with the real subscription flag and resolves the identity key. Handlers then run in order underconversation(thread_id). No dedupe, lock or routing: an update never reacheson_mention,on_subscribed_messageoron_message. Identity resolution moved into_resolve_message_identity, which_dispatch_to_handlersnow shares.MessageDeletedEvent.process_message_deleted(event, options=None)fillsplatformfromadapter.namewhen it isNone(usingdataclasses.replace). It builds no Thread and runs underconversation(thread_id).InstallationAction, plusInstallationEventwithInstalledEvent/UninstalledEvent.on_installed/on_uninstalledandprocess_installed/process_uninstalledcreate no task when no handlers are registered. Otherwise the handlers run in order underconversation(event.channel_id), which runs bare when it isNone. Errors are caught inside the coroutine and logged as"<Kind> handler error"withconversation_idandactivity_id, so thewait_untiltask always completes.wait_untilreceives the shielded_hand_to_wait_untilwrapper. This is Python-specific: it means a host that cancels the task cannot cancel the handlers.AppContextEntitydataclasses (channel/canvas/list/message/unknown,kindLiteral, snake_case fields, keyword-onlyenterprise_id/team_id),AppContextChangedEvent,on_app_context_changedandprocess_app_context_changed.AppHomeOpenedEventgainsentitiesandtab, both defaulting toNone.ChatInstanceProtocol gainsprocess_message_updated,process_message_deletedandprocess_app_context_changed.process_installed/process_uninstalledstay off it because they are optional upstream; adapters probe them withgetattr.set_suggested_prompts(channel_id, thread_ts: str | None, prompts, title=None)omitsthread_tswhen it is falsy. The request now goes throughclient.api_call(api_method="assistant.threads.setSuggestedPrompts", json=...), the same request the generated helper sends. That helper requiresthread_tsbefore slack-sdk 3.43.0, and our>=3.27.0floor allows those versions (checked against 3.27.0, 3.42.0 and 3.43.0).chat_sdk.testing.create_mock_chat_instance()portscreateMockChatInstance. Processors are recordingMagicMocks;handle_incoming_message,process_options_loadandprocess_modal_submitareAsyncMocks. It returns aSimpleNamespace, so unknown attributes raise.chat_sdk.__all__.Upstream commits mapped
4ac04551feat(chat): message update and delete lifecycle callbacks (vercel/chat#788)packages/chat2e2426d1feat(teams): installation lifecycle events (vercel/chat#914)process*,createMockChatInstanceentries (Teams emitter → #217)1721fa01feat(slack): Slack Agent messaging experience (agent_view) (vercel/chat#684)processAppContextChanged,AppHomeOpenedEvent.entities/tab, SlacksetSuggestedPromptsoptionalthreadTs(agent_view parsing → #214)Tests ported
chat.test.ts→tests/test_chat_faithful.py::TestMessageLifecycleEvents: "should dispatch message updates without normal message routing", "should dispatch message deletes with normalized event data".installation-events.test.ts→tests/test_installation_events.py: all 4 tests, parametrized over Installed/Uninstalled (upstream'sdescribe.each). Also one Python test that a raising handler stops the later ones (the upstream short-circuit), and onecreate_mock_chat_instancetest that stands in forpackages/tests/src/installation-matcher.test.ts. That file tests thetoHaveDispatchedmatcher, which has no Python equivalent.app-context.test.ts→tests/test_app_context.py: "dispatches app_context_changed events to registered handlers", plus a Python test for the channel conversation and error logging.index.test.ts→tests/test_slack_api.py::TestSetSuggestedPrompts: "omits thread_ts when not provided" (parametrized overNoneand"") and "includes thread_ts when provided".tests/test_message_lifecycle.py: skips self-edits; resolves lazymessage/previous_messagefactories (AsyncMock);active_conversation() == thread_idinside update and delete handlers; updates never reach subscribed handlers; no dedupe; identity is resolved before handlers; the handler task raises while thewait_untiltask completes (even withpropagate_handler_errors=True, as upstream); an adapter-suppliedplatformis kept.tests/test_dispatch_key_validation.py::_make_mock_chatgains the five newprocess_*names.conversation(...)wrap, installation try/except and no-handler early return, factory resolution, identity, subscription flag,wait_untilpropagation, Slackthread_tsfalsy check) killed all 14.Fidelity
TS_ROOT=<chat@4.41.1> uv run python scripts/verify_test_fidelity.py --report-target:All 7 match exactly; none rely on fuzzy matching.
--strictat thechat@4.31.0pin still passes.Review (gpt-6-astra)
d2373bc:set_suggested_promptsraisedTypeErroron slack-sdk 3.42.0. It now goes throughapi_call; the tests assert theapi_callboundary and that the helper is never called.wait_untildirectly, so a host cancelling it would cancel the handlers (upstream's promise can't be cancelled). It now goes through the shielded wrapper, with a new test that cancels thewait_untiltask.Divergences
None added to the non-parity table. These are Python API-shape adaptations, documented in
docs/UPSTREAM_SYNC.md("Core lifecycle events…"):process_message_updated(adapter, thread_id, message, *, previous_message=None, options=None)replaces upstream's single{adapter, threadId, message, previousMessage?}object. The optional tail is keyword-only, because the fourth positional slot ofprocess_messageisoptions; a positional call written by analogy now raisesTypeErrorinstead of quietly treatingoptionsasprevious_message.MessageUpdatedHandleralways receivesprevious_messageas a third argument, which isNonewhen absent. JS can simply omit it.InstalledEvent/UninstalledEventnarrowactionwith apyrefly: ignore[bad-override-mutable-attribute]. TS interfaces allow this narrowing.create_mock_chat_instanceleaves out upstream'sabortTurnand the agent-session processors, which Python doesn't have yet ([4.41/C7] Turn cancellation: abort_turn, thread.signal, typing options, agent-session events #201).Consumer impact
Low and additive. New handlers, entry points and types.
AppHomeOpenedEvent's new fields have defaults. Two changes are visible to consumers:ChatInstancefakes that lack the three new methods no longer passisinstance(x, ChatInstance), because the Protocol is@runtime_checkable. Nothing in the SDK makes that check.set_suggested_promptsnow acceptsthread_ts=Noneand leaves it out of the request.Both are noted in the CHANGELOG under "Unreleased (4.41 wave)".
Validation
ruff check, ruff format --check, audit_test_quality (0 hard failures),
verify_test_fidelity --check-docs,--strictat the pin, pytest (6657 passed, 24 skipped, after merging origin/main @ c4545f9) andpyrefly check(0 errors) all pass.Closes #196
Part of #184
Merge gate
Independent review findings (4). All were verified and fixed in
1d88189; every new test fails on the pre-fix code (8 targeted mutants, 8 killed):test_mock_chat_instance_records_app_context_changed(tests/test_app_context.py) asserts thatcreate_mock_chat_instance()recordsprocess_app_context_changed(event). The UPSTREAM_SYNC.md "Testing" bullet now namesapp-context-matcher.test.tsalongsideinstallation-matcher.test.ts.test_binds_adapter_so_message_subject_resolvesandtest_binds_adapter_so_previous_message_subject_resolvesawaitmessage.subjectinside the handler and assert thatadapter.fetch_subjectwas awaited. Removing eitherset_message_adaptercall now fails a test. Upstream does the same (chat.ts:2503, 2552-2554).create_mock_chat_instancecontract was untested. Fixed.test_mock_chat_instance_async_processors_overrides_and_accessorscovers four things: the three async processors are awaitable and resolve toNone, as in upstream factories.ts:284-288;overridesreplaces attributes;get_state()returns the supplied state;get_user_name()returns the supplied user name.process_message_updated. Fixed.previous_messageandoptionsare now keyword-only on bothChatand theChatInstanceProtocol.test_optional_tail_is_keyword_onlychecks that a positionalWebhookOptionsraisesTypeErrorand schedules no task. No caller in src/ or on any siblingsync/4.41-*branch passed these positionally.gpt-6-astra: this convergence pass ran 2 rounds, on
1d88189and then on the post-merge HEADac24d6c. Both were PASS with no actionable findings. The final verdict is on the final HEAD.Bots: CodeRabbit was rate-limited and posted no review findings. Gemini left no comments. Nothing was actionable.
CI: all checks are green on
ac24d6c: Lint & Type Check, test (3.12), test (3.13), CodeQL. Locally, the full validation is green: ruff check/format, audit_test_quality (0 hard failures), fidelity--check-docs,--strict733/733 at chat@4.31.0, pytest (6657 passed, 24 skipped) and pyrefly (0 errors).