Skip to content

feat(chat): core lifecycle events — message updated/deleted, installed/uninstalled, app context changed (#196) - #272

Merged
patrick-chinchill merged 9 commits into
mainfrom
sync/4.41-c4
Oct 1, 2026
Merged

patrick-chinchill merged 9 commits into
mainfrom
sync/4.41-c4

Conversation

@patrick-chinchill

@patrick-chinchill patrick-chinchill commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

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.

  • Message updates: chat.on_message_updated(handler) is called as handler(thread, message, previous_message). Chat.process_message_updated(adapter, thread_id, message, previous_message=None, options=None) accepts each message as a Message or an async factory. It binds the adapter, skips author.is_me, builds the Thread with the real subscription flag and resolves the identity key. Handlers then run in order under conversation(thread_id). No dedupe, lock or routing: an update never reaches on_mention, on_subscribed_message or on_message. Identity resolution moved into _resolve_message_identity, which _dispatch_to_handlers now shares.
  • Message deletes: new MessageDeletedEvent. process_message_deleted(event, options=None) fills platform from adapter.name when it is None (using dataclasses.replace). It builds no Thread and runs under conversation(thread_id).
  • Installation: new InstallationAction, plus InstallationEvent with InstalledEvent / UninstalledEvent. on_installed / on_uninstalled and process_installed / process_uninstalled create no task when no handlers are registered. Otherwise the handlers run in order under conversation(event.channel_id), which runs bare when it is None. Errors are caught inside the coroutine and logged as "<Kind> handler error" with conversation_id and activity_id, so the wait_until task always completes. wait_until receives the shielded _hand_to_wait_until wrapper. This is Python-specific: it means a host that cancels the task cannot cancel the handlers.
  • App context (agent_view): the AppContextEntity dataclasses (channel/canvas/list/message/unknown, kind Literal, snake_case fields, keyword-only enterprise_id / team_id), AppContextChangedEvent, on_app_context_changed and process_app_context_changed. AppHomeOpenedEvent gains entities and tab, both defaulting to None.
  • ChatInstance Protocol gains process_message_updated, process_message_deleted and process_app_context_changed. process_installed / process_uninstalled stay off it because they are optional upstream; adapters probe them with getattr.
  • Slack: set_suggested_prompts(channel_id, thread_ts: str | None, prompts, title=None) omits thread_ts when it is falsy. The request now goes through client.api_call(api_method="assistant.threads.setSuggestedPrompts", json=...), the same request the generated helper sends. That helper requires thread_ts before slack-sdk 3.43.0, and our >=3.27.0 floor allows those versions (checked against 3.27.0, 3.42.0 and 3.43.0).
  • Testing: chat_sdk.testing.create_mock_chat_instance() ports createMockChatInstance. Processors are recording MagicMocks; handle_incoming_message, process_options_load and process_modal_submit are AsyncMocks. It returns a SimpleNamespace, so unknown attributes raise.
  • All new types and handler aliases are exported from chat_sdk.__all__.

Upstream commits mapped

Upstream Release Ported here
4ac04551 feat(chat): message update and delete lifecycle callbacks (vercel/chat#788) chat@4.37.0 everything in packages/chat
2e2426d1 feat(teams): installation lifecycle events (vercel/chat#914) chat@4.41.0 core types, handlers, process*, createMockChatInstance entries (Teams emitter → #217)
1721fa01 feat(slack): Slack Agent messaging experience (agent_view) (vercel/chat#684) chat@4.34.0 core types, handlers, processAppContextChanged, AppHomeOpenedEvent.entities/tab, Slack setSuggestedPrompts optional threadTs (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's describe.each). Also one Python test that a raising handler stops the later ones (the upstream short-circuit), and one create_mock_chat_instance test that stands in for packages/tests/src/installation-matcher.test.ts. That file tests the toHaveDispatched matcher, 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.
  • Slack index.test.ts → tests/test_slack_api.py::TestSetSuggestedPrompts: "omits thread_ts when not provided" (parametrized over None and "") and "includes thread_ts when provided".
  • Python-specific coverage in tests/test_message_lifecycle.py: skips self-edits; resolves lazy message / previous_message factories (AsyncMock); active_conversation() == thread_id inside update and delete handlers; updates never reach subscribed handlers; no dedupe; identity is resolved before handlers; the handler task raises while the wait_until task completes (even with propagate_handler_errors=True, as upstream); an adapter-supplied platform is kept.
  • tests/test_dispatch_key_validation.py::_make_mock_chat gains the five new process_* names.
  • A mutation pass over 14 mutants of the new code (self-skip, platform normalization, each conversation(...) wrap, installation try/except and no-handler early return, factory resolution, identity, subscription flag, wait_until propagation, Slack thread_ts falsy check) killed all 14.

Fidelity

TS_ROOT=<chat@4.41.1> uv run python scripts/verify_test_fidelity.py --report-target:

Delta vs committed report (origin/main @ f2cf21b): missing 167 -> 160 (-7)
  packages/chat/src/app-context.test.ts: 1 -> 0 (-1)
  packages/chat/src/chat.test.ts: 8 -> 6 (-2)
  packages/chat/src/installation-events.test.ts: 4 -> 0 (-4)

All 7 match exactly; none rely on fuzzy matching. --strict at the chat@4.31.0 pin still passes.

Review (gpt-6-astra)

  • Round 1 raised two P2s. Both were Python-specific hazards, so both are fixed in d2373bc:
    • Threadless set_suggested_prompts raised TypeError on slack-sdk 3.42.0. It now goes through api_call; the tests assert the api_call boundary and that the helper is never called.
    • The installation task was handed to wait_until directly, 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 the wait_until task.
    • A mutation check confirms the new tests fail on both pre-fix versions.
  • Round 2: PASS. No actionable findings.

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 of process_message is options; a positional call written by analogy now raises TypeError instead of quietly treating options as previous_message.
  • MessageUpdatedHandler always receives previous_message as a third argument, which is None when absent. JS can simply omit it.
  • InstalledEvent / UninstalledEvent narrow action with a pyrefly: ignore[bad-override-mutable-attribute]. TS interfaces allow this narrowing.
  • create_mock_chat_instance leaves out upstream's abortTurn and 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:

  1. Custom ChatInstance fakes that lack the three new methods no longer pass isinstance(x, ChatInstance), because the Protocol is @runtime_checkable. Nothing in the SDK makes that check.
  2. Slack set_suggested_prompts now accepts thread_ts=None and 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, --strict at the pin, pytest (6657 passed, 24 skipped, after merging origin/main @ c4545f9) and pyrefly 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):

  1. app-context-matcher.test.ts not accounted for. Fixed. test_mock_chat_instance_records_app_context_changed (tests/test_app_context.py) asserts that create_mock_chat_instance() records process_app_context_changed(event). The UPSTREAM_SYNC.md "Testing" bullet now names app-context-matcher.test.ts alongside installation-matcher.test.ts.
  2. Adapter bind in the update/delete paths was untested. Fixed. test_binds_adapter_so_message_subject_resolves and test_binds_adapter_so_previous_message_subject_resolves await message.subject inside the handler and assert that adapter.fetch_subject was awaited. Removing either set_message_adapter call now fails a test. Upstream does the same (chat.ts:2503, 2552-2554).
  3. create_mock_chat_instance contract was untested. Fixed. test_mock_chat_instance_async_processors_overrides_and_accessors covers four things: the three async processors are awaitable and resolve to None, as in upstream factories.ts:284-288; overrides replaces attributes; get_state() returns the supplied state; get_user_name() returns the supplied user name.
  4. Positional-misuse hazard in process_message_updated. Fixed. previous_message and options are now keyword-only on both Chat and the ChatInstance Protocol. test_optional_tail_is_keyword_only checks that a positional WebhookOptions raises TypeError and schedules no task. No caller in src/ or on any sibling sync/4.41-* branch passed these positionally.

gpt-6-astra: this convergence pass ran 2 rounds, on 1d88189 and then on the post-merge HEAD ac24d6c. 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, --strict 733/733 at chat@4.31.0, pytest (6657 passed, 24 skipped) and pyrefly (0 errors).

…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
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: ddfc791a-03c3-4adb-8d15-7b5044e0c292

📥 Commits

Reviewing files that changed from the base of the PR and between f2cf21b and aec9c4a.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • docs/UPSTREAM_SYNC.md
  • scripts/fidelity_target.json
  • src/chat_sdk/__init__.py
  • src/chat_sdk/adapters/slack/adapter.py
  • src/chat_sdk/chat.py
  • src/chat_sdk/shared/mock_adapter.py
  • src/chat_sdk/testing/__init__.py
  • src/chat_sdk/types.py
  • tests/test_app_context.py
  • tests/test_chat_faithful.py
  • tests/test_dispatch_key_validation.py
  • tests/test_installation_events.py
  • tests/test_message_lifecycle.py
  • tests/test_shared_download.py
  • tests/test_slack_api.py
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… 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)
@patrick-chinchill
patrick-chinchill marked this pull request as ready for review October 1, 2026 05:02
@patrick-chinchill

Copy link
Copy Markdown
Collaborator Author

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 history so it failed the runtime_checkable ChatInstance -> fixed (829e823, with isinstance test); r2 [P2] bind previous_message to adapter in update dispatch -> rebutted as upstream parity (chat@4.41.1 chat.ts:2503 binds only message; message.ts:191-198), parity comment added (5fc83c6); r3 clean; r4 clean after a test-only deadline widening (aec9c4a) for test_shared_download hanging-close test that failed deterministically on CI 3.12+coverage (50ms -> 500ms success budget). Fidelity target delta vs main f2cf21b: missing 167 -> 160 (-7); strict 733/733. CodeRabbit: review rate limited (no findings). Merging with --admin (Protect Main requires a code-owner approval).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[4.41/C4] Core lifecycle events: message updated/deleted, installed/uninstalled, app context changed

1 participant