Skip to content

[4.41/SL10] Slack Agent Sessions lifecycle + native stop #215

Closed

Description

@patrick-chinchill

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

  • 2ce2be00 feat(slack): add Agent Sessions lifecycle and native stop (#862) — chat@4.39.0. Slack half:
    • supportsTurnCancellation = agentView.
    • agent_session_stopped runs chat.abortTurn(threadId), sets the session to active and calls processAgentSessionStopped, all in a waitUntil task that does not take the message lock.
    • agent_session_title_changed calls processAgentSessionTitleChanged.
    • setSessionStatus / renameAgentSession use apiCall.
    • startTyping(threadId, status?, {initiatorUserId}) maps to processing.
    • New endTyping(threadId, status="active").
    • setAssistantTitle renames under agentView.
    • The stream loop breaks on options.signal.aborted, and stopStream carries session_status (default active) followed by endTyping.
    • Automatic session titles: sessionTitle config (defaults to agentView; 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.
  • 2cc8cc3f fix(slack): surface custom status text in the Agent messaging experience (#897) — chat@4.40.0 — under agentView, a custom status goes through the legacy assistant.threads.setStatus (loading_messages = explicit, then configured, then [status]). An empty status maps to session active. startTyping without a status keeps native processing.
  • 8b6d7f3a fix(slack): clear Agent Session typing status (#882) — chat@4.40.0 — startTyping(threadId, "") sets session active instead of processing.

Current Python behavior

  • grep -rn 'agents.sessions\|agent_session_stopped\|abort_turn\|supports_turn_cancellation\|end_typing\|session_status\|session_title' src/chat_sdk finds 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-3878 start_typing(thread_id, status=None) always calls assistant_threads_setStatus, with no TypingOptions.
  • adapter.py:2896-2917 set_assistant_status / set_assistant_title use only the assistant.threads.* endpoints.
  • adapter.py:3880-4060 stream(): the loops at :3913 and :4023 never check a cancellation signal, and streamer.stop(**stop_kwargs) at :4056 has no session_status.
  • src/chat_sdk/types.py:1056 StreamOptions has no signal / session_status.
  • _process_event_payload (adapter.py:1710-1727) has no agent_session_* branches.
  • slack_sdk 3.41 has no agents_sessions_* methods, but AsyncWebClient.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) via client.api_call("agents.sessions.setStatus" | "agents.sessions.rename", json=...), including the [4.41/SL8] Slack: guarded downloads, Enterprise Grid org-wide installs, egress proxy #213 team_id kwargs helper if it has landed.
  • supports_turn_cancellation property that returns self._agent_view.
  • start_typing(thread_id, status=None, options: TypingOptions | None = None):
    • under agent_view with no status or an empty one, call set_session_status("active" if status == "" else "processing", initiator_user_id=...) and log failures as warnings;
    • otherwise use legacy setStatus with status if status is not None else (configured[0] if configured else "Typing...") and loading_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).
  • end_typing(thread_id, status="active") does nothing unless agent_view is set and a thread ts exists.
  • set_assistant_status: under agent_view an empty or whitespace-only status (not status.strip()) sets session active. Otherwise legacy setStatus with loading_messages = first non-None of explicit, configured, then [status] under agent_view (else omitted).
  • set_assistant_title: under agent_view, call _rename_agent_session.
  • stream(): at the top of every chunk-loop iteration, if options.signal is aborted, break. Under agent_view only: pass session_status (options.session_status if not None, else "active") to the final streamer.stop; pass session_status="processing" on [4.41/SL3] Slack: rotate long native streams before expiry #208's rotation stop (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, call end_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, and agent_session_title_changed → _handle_agent_session_title_changed.
  • _handle_agent_session_stopped: build the thread id from channel + thread_ts, then run a task that does await chat.abort_turn(thread_id) (warn on error), then set_session_status(..., "active") (warn on error), then chat.process_agent_session_stopped(...) with streaming_message_ts. Hand it to wait_until, and do not take the thread lock.
  • _handle_agent_session_title_changed: call process_agent_session_title_changed (title, previous_title, thread_ts, user_id).
  • session_title config (bool | Callable[[SlackSessionTitleContext], str | None | Awaitable[str | None]], default agent_view) and _apply_configured_session_title(event). It applies only to top-level human DMs that have text, no bot_id and no subtype. 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_message on the normal agent_view DM path (warn if processing failed).

Out of scope

Porting notes

  • Cancellation: upstream's AbortSignal maps to whatever [4.41/C7] Turn cancellation: abort_turn, thread.signal, typing options, agent-session events #201 landed (recommended: an asyncio.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, because CancelledError would skip streamer.stop and 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; CancelledError must propagate.
  • status == "" versus None is load-bearing (8b6d7f3a). Never collapse it with or. Use status is None / status == "" explicitly.
  • session_status goes on the wire as a snake_case kwarg to streamer.stop only under agent_view, which keeps legacy payloads byte-identical.
  • The title resolver may be sync or async (inspect.isawaitable). None means skip. Emulate text.split("\n", 1)[0].strip()[:80].
  • api_call responses: slack_sdk raises SlackApiError on ok: 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 no agents.sessions.* call from start_typing/stream;
    • start_typing(tid, "") versus start_typing(tid) send different statuses;
    • when the signal is set mid-stream, streamer.stop is still awaited exactly once (AsyncMock.assert_awaited_once);
    • the stop handler runs while another handler holds the thread lock (no deadlock; use asyncio.Event gates, not sleeps);
    • the title is truncated to 80 characters.

Acceptance criteria

  • Full validation command from CLAUDE.md passes.
  • docs/UPSTREAM_SYNC.md updated. 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=False opts out). There is no change without agent_view.
  • A live check in an agent_view workspace confirms the Stop button cancels a streaming reply and the session returns to active, 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-events for PR B)

Part of #184.

Activity

  1. patrick-chinchill commented on Oct 1, 2026

    @patrick-chinchill
    CollaboratorAuthor

    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.1 packages/adapter-slack/src/index.ts:6508). It carries a # SL10: marker in slack/adapter.py, next to the rotation stop() marker for session_status="processing". grep -n 'SL10' src/chat_sdk/adapters/slack/adapter.py lists both.

  2. patrick-chinchill commented on Oct 1, 2026

    @patrick-chinchill
    CollaboratorAuthor

    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 adopts agent_view.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions