Repository navigation
[4.41/SL10] Slack Agent Sessions lifecycle + native stop #215
Description
Summary
Under agent_view, Slack manages conversations as Agent Sessions: status and titles go through agents.sessions.setStatus / agents.sessions.rename, the native Stop button fires agent_session_stopped (which should cancel the in-flight turn), and title edits fire agent_session_title_changed. This ports the Slack half of upstream #862 plus two follow-up fixes, on top of #201's turn-cancellation primitives and #214's agent_view plumbing. Active only when agent_view=True; the legacy assistant_view path stays unchanged. XL, so two PRs; it may trail 0.4.41 as a documented non-parity row.
Upstream changes
2ce2be00feat(slack): add Agent Sessions lifecycle and native stop (#862) — chat@4.39.0. Slack half:supportsTurnCancellation = agentView.agent_session_stoppedrunschat.abortTurn(threadId), sets the session toactiveand callsprocessAgentSessionStopped, all in awaitUntiltask that does not take the message lock.agent_session_title_changedcallsprocessAgentSessionTitleChanged.setSessionStatus/renameAgentSessionuseapiCall.startTyping(threadId, status?, {initiatorUserId})maps toprocessing.- New
endTyping(threadId, status="active"). setAssistantTitlerenames underagentView.- The stream loop breaks on
options.signal.aborted, andstopStreamcarriessession_status(defaultactive) followed byendTyping. - Automatic session titles:
sessionTitleconfig (defaults toagentView;true= first line of the root message, trimmed to 80 chars; or a resolver) are applied after processing a top-level human DM, both in the bridge and on the normal path.
2cc8cc3ffix(slack): surface custom status text in the Agent messaging experience (#897) — chat@4.40.0 — underagentView, a custom status goes through the legacyassistant.threads.setStatus(loading_messages= explicit, then configured, then[status]). An empty status maps to sessionactive.startTypingwithout a status keeps nativeprocessing.8b6d7f3afix(slack): clear Agent Session typing status (#882) — chat@4.40.0 —startTyping(threadId, "")sets sessionactiveinstead ofprocessing.
Current Python behavior
grep -rn 'agents.sessions\|agent_session_stopped\|abort_turn\|supports_turn_cancellation\|end_typing\|session_status\|session_title' src/chat_sdkfinds nothing outside Linear. [4.41/C7] Turn cancellation: abort_turn, thread.signal, typing options, agent-session events #201 adds the core APIs.src/chat_sdk/adapters/slack/adapter.py:3845-3878start_typing(thread_id, status=None)always callsassistant_threads_setStatus, with noTypingOptions.adapter.py:2896-2917set_assistant_status/set_assistant_titleuse only theassistant.threads.*endpoints.adapter.py:3880-4060stream(): the loops at:3913and:4023never check a cancellation signal, andstreamer.stop(**stop_kwargs)at:4056has nosession_status.src/chat_sdk/types.py:1056StreamOptionshas nosignal/session_status._process_event_payload(adapter.py:1710-1727) has noagent_session_*branches.- slack_sdk 3.41 has no
agents_sessions_*methods, butAsyncWebClient.api_call(api_method, json=...)exists.AsyncChatStream.stop(**kwargs)forwards unknown kwargs.
Scope
PR A: lifecycle calls (no new events)
-
set_session_status(channel_id, thread_ts, status, *, initiator_user_id=None, title=None)and_rename_agent_session(channel_id, thread_ts, title)viaclient.api_call("agents.sessions.setStatus" | "agents.sessions.rename", json=...), including the [4.41/SL8] Slack: guarded downloads, Enterprise Grid org-wide installs, egress proxy #213team_idkwargs helper if it has landed. -
supports_turn_cancellationproperty that returnsself._agent_view. -
start_typing(thread_id, status=None, options: TypingOptions | None = None):- under
agent_viewwith no status or an empty one, callset_session_status("active" if status == "" else "processing", initiator_user_id=...)and log failures as warnings; - otherwise use legacy
setStatuswithstatus if status is not None else (configured[0] if configured else "Typing...")andloading_messages = [status] if status else (configured or ["Typing..."])(keep [4.41/SL9] Slack agent_view (Agent messaging experience) + declarative agent config #214's form if already landed).
- under
-
end_typing(thread_id, status="active")does nothing unlessagent_viewis set and a thread ts exists. -
set_assistant_status: underagent_viewan empty or whitespace-only status (not status.strip()) sets sessionactive. Otherwise legacysetStatuswithloading_messages= first non-Noneof explicit, configured, then[status]underagent_view(else omitted). -
set_assistant_title: underagent_view, call_rename_agent_session. -
stream(): at the top of every chunk-loop iteration, ifoptions.signalis aborted, break. Underagent_viewonly: passsession_status(options.session_statusif notNone, else"active") to the finalstreamer.stop; passsession_status="processing"on [4.41/SL3] Slack: rotate long native streams before expiry #208's rotationstop(fill SL3's# SL10:marker); in [4.41/SL2] Slack streaming: stream() returns None without context, native_streaming config with post+edit fallback #207's post+edit fallback, callend_typing(thread_id, <same session_status>)before returning. Whichever of SL2/SL3/SL10 lands later wires the others' hooks.
PR B: events and titles
-
_process_event_payload:agent_session_stopped→_handle_agent_session_stopped, andagent_session_title_changed→_handle_agent_session_title_changed. -
_handle_agent_session_stopped: build the thread id fromchannel+thread_ts, then run a task that doesawait chat.abort_turn(thread_id)(warn on error), thenset_session_status(..., "active")(warn on error), thenchat.process_agent_session_stopped(...)withstreaming_message_ts. Hand it towait_until, and do not take the thread lock. -
_handle_agent_session_title_changed: callprocess_agent_session_title_changed(title,previous_title,thread_ts,user_id). -
session_titleconfig (bool | Callable[[SlackSessionTitleContext], str | None | Awaitable[str | None]], defaultagent_view) and_apply_configured_session_title(event). It applies only to top-level human DMs that have text, nobot_idand nosubtype. Trim, take the first line (or the resolver result), cut to 80 characters, and skip if empty. Catch and log all errors. - Wire the title into the [4.41/SL9] Slack agent_view (Agent messaging experience) + declarative agent config #214 bridge hook, and schedule it after
process_messageon the normal agent_view DM path (warn if processing failed).
Out of scope
- Core
Chat.abort_turn, cross-process cancellation via state,thread.signal,TypingOptions.initiator_user_id,StreamOptions.signal/session_status,StreamingPlan(session_status=),AgentSessionStatusandon_agent_session_stopped/on_agent_session_title_changedall belong to [4.41/C7] Turn cancellation: abort_turn, thread.signal, typing options, agent-session events #201. - The
agent_viewflag, bridge andloading_messagesconfig belong to [4.41/SL9] Slack agent_view (Agent messaging experience) + declarative agent config #214. - Native-stream fallback and rotation belong to [4.41/SL2] Slack streaming: stream() returns None without context, native_streaming config with post+edit fallback #207/[4.41/SL3] Slack: rotate long native streams before expiry #208.
Porting notes
- Cancellation: upstream's
AbortSignalmaps to whatever [4.41/C7] Turn cancellation: abort_turn, thread.signal, typing options, agent-session events #201 landed (recommended: anasyncio.Event-backed token with.aborted). Check it at the top of each loop iteration before awaiting the next append. Do not cancel the consuming task from outside, becauseCancelledErrorwould skipstreamer.stopand leave Slack's stream open. Break, then stop cleanly. - The stop-event task must not call into the message lock path. Keep a strong reference to the task, and add a done-callback that logs;
CancelledErrormust propagate. status == ""versusNoneis load-bearing (8b6d7f3a). Never collapse it withor. Usestatus is None/status == ""explicitly.session_statusgoes on the wire as a snake_case kwarg tostreamer.stoponly underagent_view, which keeps legacy payloads byte-identical.- The title resolver may be sync or async (
inspect.isawaitable).Nonemeans skip. Emulatetext.split("\n", 1)[0].strip()[:80]. api_callresponses:slack_sdkraisesSlackApiErroronok: false, so catch it where upstream catches.
Tests
Upstream packages/adapter-slack/src/index.test.ts is not fidelity-mapped. The core tests (agent-session.test.ts, chat/thread cases) belong to #201.
- PR A: "surfaces custom status text via the legacy API and clears via the sessions lifecycle", "routes custom labels and clearing separately on an agent_view DM thread", "does not fall back to a legacy label when native status fails", "renames an agent session under agentView", "finishes agent streams with an active session status", and (deferred here by [4.41/SL3] Slack: rotate long native streams before expiry #208) "keeps the agent session processing while the reply continues". Upstream's 4.39 test "sets status on an agent_view DM thread" was renamed in #882 and then replaced in #897 by "routes custom labels and clearing separately on an agent_view DM thread"; do not port it separately.
- PR B: "aborts and activates an agent session when the user stops it", "dispatches agent session title changes", "automatically titles a top-level agent_view DM session", "skips automatic titles when sessionTitle is disabled", "does not retitle an agent_view DM follow-up".
- Python-specific:
- legacy mode (
agent_view=False) makes noagents.sessions.*call fromstart_typing/stream; start_typing(tid, "")versusstart_typing(tid)send different statuses;- when the signal is set mid-stream,
streamer.stopis still awaited exactly once (AsyncMock.assert_awaited_once); - the stop handler runs while another handler holds the thread lock (no deadlock; use
asyncio.Eventgates, not sleeps); - the title is truncated to 80 characters.
- legacy mode (
Acceptance criteria
- Full validation command from CLAUDE.md passes.
-
docs/UPSTREAM_SYNC.mdupdated. If PR B (or the whole issue) trails 0.4.41, add a non-parity row listing the missing events and titles. - CHANGELOG entry under "Unreleased (4.41 wave)".
- Consumer-visible changes called out. With
agent_view=True, typing and title calls switch to the Agent Sessions API, the Stop button cancels turns, and top-level DMs are auto-titled by default (session_title=Falseopts out). There is no change withoutagent_view. - A live check in an
agent_viewworkspace confirms the Stop button cancels a streaming reply and the session returns toactive, and the result is recorded in the PR.
Dependencies
Blocked by #201 and #214. It touches the same stream() code as #207/#208, so rebase onto whichever has landed.
Metadata
- Effort: XL, split into PR A (M, about 500 LOC) and PR B (M, about 600 LOC)
- Consumer impact: low today (opt-in). It becomes high for streaming consumers once they adopt
agent_view(required by Feb 2027), because it changes typing, stop and stream completion. - Suggested branch:
sync/4.41-sl10(sync/4.41-sl10-eventsfor PR B)
Part of #184.
Activity
patrick-chinchill commentedon Oct 1, 2026
Note from #208 / PR #302: native stream rotation adds one more stream return that needs
end_typing(thread_id, session_status)when this lands: the final-stop expiry path that returns the already-finalized message (upstream chat@4.41.1packages/adapter-slack/src/index.ts:6508). It carries a# SL10:marker inslack/adapter.py, next to the rotationstop()marker forsession_status="processing".grep -n 'SL10' src/chat_sdk/adapters/slack/adapter.pylists both.fix(slack): rotate long native streams before Slack expires them (#208)
feat(chat): turn cancellation, thread.signal, typing lifecycle, agent…
fix(slack): close the stream input when an aborted turn breaks the lo…
fix(slack): make the aborted-stream input close best-effort (#215)
test(slack): pin first-line titles, subtype skip, legacy status paylo…
fix(slack): route agents.sessions.* through _with_token_kwargs after #…
feat(slack): Agent Sessions lifecycle, native stop and automatic titl…
patrick-chinchill commentedon Oct 1, 2026
Closing: shipped in 0.4.41 (#303). Opt-in and not live-verified: Slack Agent Sessions native Stop only runs for a Slack app with the Agent messaging experience enabled and
agent_view=True. No current consumer enables it; chinchill uses its own cancel button, which was live-tested and works. The ported upstream tests cover it, and the 0.4.41 CHANGELOG "Live validation" section records it as not live-verified (#311). Reopen if a consumer adoptsagent_view.