You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(slack): route empty-DM thread_ts fetch to conversations.history (#138) (#175)
A top-level Slack DM root encodes an empty thread_ts (slack:Dxxx:). The
fetch paths previously called conversations.replies(ts="") unconditionally,
which returns no replies and loses the DM root context for every DM.
- fetch_messages: when thread_ts is empty (falsy), route to the existing
channel-history helpers (_fetch_channel_messages_forward /
_fetch_channel_messages_backward, both conversations.history) instead of
the thread-reply helpers. Direction, limit, and cursor semantics are
preserved; for a DM the channel IS the conversation, so this is correct.
- fetch_message: when thread_ts is empty, fetch the single message via
conversations.history(latest=message_id, inclusive=True, limit=1),
mirroring the inner link-preview fetch_message.
- Non-empty thread_ts stays byte-identical (still conversations.replies).
This is a divergence ahead of upstream — upstream's fetchMessages /
fetchMessage call conversations.replies(ts: threadTs) with no empty-thread_ts
guard either. Documented in docs/UPSTREAM_SYNC.md as a candidate to file
upstream; it also covers the #137 DM block-action consumer uniformly.
Tests: empty-DM fetch_messages (forward + backward) and fetch_message assert
conversations.history is used and conversations.replies(ts="") is never
called; non-empty thread_ts regression guards assert conversations.replies is
still used (so the routing cannot over-trigger).
Copy file name to clipboardExpand all lines: docs/UPSTREAM_SYNC.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -667,7 +667,8 @@ stay explicit instead of being rediscovered in code review.
667
667
|`_rehydrate_message` with `Message` input | Falls through to the `rehydrate_attachment` pass even when the dequeued entry is already a `Message` instance | Early-returns on `raw instanceof Message` before rehydration | The Python port's Redis + Postgres `dequeue()` upgrade raw JSON to `Message.from_json(...)` before returning (upstream's dequeue returns the raw JSON.parse'd dict). Upstream's `instanceof Message` shortcut therefore only fires for in-memory state, but ours would fire for persistent backends too, leaving `fetch_data` stripped forever. The rehydrate pass still skips any attachment that already has `fetch_data`, so in-memory callers pay no cost. |
668
668
| Slack Socket Mode reconnect loop | Outer reconnect loop on top of `slack_sdk.socket_mode.aiohttp.SocketModeClient` (which itself has `auto_reconnect_enabled=True`). Exponential backoff (1s → 30s) with explicit shutdown signaling and a tracked `asyncio.Task` so `disconnect()` can cancel cleanly | Single `SocketModeClient` instance from `@slack/socket-mode`; relies entirely on the package's internal reconnect | Hazard #5 (async task lifecycle): a long-lived WebSocket needs an explicit shutdown path so `disconnect()` doesn't leak the loop, and a guarded outer reconnect path so the adapter survives `connect()` itself raising (which the inner client doesn't retry). Inner auto-reconnect still runs; the outer loop is belt-and-suspenders, not a divergence in observable behavior. |
669
669
| Slack Socket Mode listener serverless variant | Not ported |`startSocketModeListener()` / `runSocketModeListener()` open a transient socket for `durationMs` and forward events via HTTP POST | Vercel-specific pattern (cron-triggered ephemeral listener with `waitUntil`). The forwarded-event receiver (`x-slack-socket-token` handling in `handle_webhook`) is ported so a separate Python process can run the long-lived listener; the deployment glue itself isn't part of the SDK. |
670
-
| Slack DM block-action threading (#133/#137) | `_handle_block_actions` sets `thread_ts=""` for a top-level DM button click (never falls back to the clicked message's own `ts`), so a handler's `event.thread.post(...)` does not spawn a phantom "1 reply" thread in the DM. Mirrors `_handle_message_event`'s DM handling (`thread_ts=""` for top-level DMs). | `handleBlockActions` (`adapter-slack/src/index.ts:1455-1456,1470`) computes `thread_ts \|\| container.thread_ts \|\| messageTs` and encodes `threadTs \|\| messageTs \|\| ""` — it falls back to the clicked message's `ts` even for DMs, so a DM button click spawns a phantom reply thread. Upstream's `handleMessageEvent` *does* empty-case DMs (`:2158`), but `handleBlockActions` does **not** — an upstream internal inconsistency. | Hard UX failure with no workaround (phantom "1 reply" threads on DM button clicks). We extend upstream's own DM-message convention to the block-action path. The resulting empty DM `thread_ts` is consumed unguarded by `fetch_messages` → `conversations.replies(ts="")` — identical to upstream (`fetchMessages` `:4178` has no empty-`thread_ts` guard) and to the faithful DM-message path; a `conversations.history` fallback for empty DM `thread_ts` is a separate, codebase-wide follow-up. The block-action fix should be contributed upstream (cf. PR #107's stream() divergence) to restore parity. |
670
+
| Slack DM block-action threading (#133/#137) | `_handle_block_actions` sets `thread_ts=""` for a top-level DM button click (never falls back to the clicked message's own `ts`), so a handler's `event.thread.post(...)` does not spawn a phantom "1 reply" thread in the DM. Mirrors `_handle_message_event`'s DM handling (`thread_ts=""` for top-level DMs). | `handleBlockActions` (`adapter-slack/src/index.ts:1455-1456,1470`) computes `thread_ts \|\| container.thread_ts \|\| messageTs` and encodes `threadTs \|\| messageTs \|\| ""` — it falls back to the clicked message's `ts` even for DMs, so a DM button click spawns a phantom reply thread. Upstream's `handleMessageEvent` *does* empty-case DMs (`:2158`), but `handleBlockActions` does **not** — an upstream internal inconsistency. | Hard UX failure with no workaround (phantom "1 reply" threads on DM button clicks). We extend upstream's own DM-message convention to the block-action path. The resulting empty DM `thread_ts` is consumed by `fetch_messages` → now routed to `conversations.history` for empty `thread_ts` (see the #138 row below); the block-action fix should be contributed upstream (cf. PR #107's stream() divergence) to restore parity. |
671
+
| Slack empty-DM `thread_ts` fetch routing (#138) | `fetch_messages` routes an empty (falsy) `thread_ts` — every top-level DM root, encoded `slack:Dxxx:` — to the channel-history path (`_fetch_channel_messages_forward` / `_fetch_channel_messages_backward`, both `conversations.history`) instead of `conversations.replies(ts="")`, preserving direction/limit/cursor. `fetch_message` likewise reads a single empty-`thread_ts` message via `conversations.history(channel, latest=message_id, inclusive=True, limit=1)` (mirroring the inner link-preview `fetch_message` at `slack/adapter.py:3293`). Non-empty `thread_ts` stays byte-identical on `conversations.replies`. | `fetchMessages` (`adapter-slack/src/index.ts:4135` → `fetchMessagesForward`/`Backward` `:4187`/`:4250`) and `fetchMessage` (`:4350`) call `conversations.replies({ ts: threadTs })` with **no** empty-`thread_ts` guard. With `ts=""` Slack returns no replies and the DM root context is lost. | Hard UX failure on **every** DM root: history/single-message fetches over a DM silently return nothing (or lose the root) because the DM root legitimately encodes `threadTs=""` (faithful to `_handle_message_event` / `handleMessageEvent`, "matches openDM subscriptions"). For a DM the channel **is** the conversation, so `conversations.history` is the correct source. This supersedes the "separate follow-up" noted in the #133/#137 row and covers DM message fetches **and** the #137 DM block-action consumer uniformly. Candidate to file upstream against vercel/chat (an empty-`thread_ts` guard in `fetchMessages`/`fetchMessage`); remove this divergence once upstream adds it. |
671
672
|`GitHubAdapter.octokit` native client getter (vercel/chat#459, #478) | Not exposed |`get octokit(): Octokit` (plus deprecated `client` alias) returns the underlying Octokit — fixed instance in PAT/single-tenant App mode, per-installation client resolved from `AsyncLocalStorage` inside a webhook handler in multi-tenant mode | The Python adapter is hand-rolled over raw `aiohttp` (`_github_api_request`) with PyJWT for App JWTs and an installation-token cache; the `github` extra is `pyjwt[crypto]` only — there is no Octokit-equivalent object to return, and exposing the raw session or an invented facade under the name `octokit` would misrepresent the surface. Revisit if the adapter adopts an octokit-style SDK (e.g. `githubkit`) as an optional dependency per hazard #10's "prefer official SDKs" sub-rule; the getter (and the GitHub `fetch_subject` half of #459) ports cleanly then. |
672
673
|`LinearAdapter.linear_client` native client getter (vercel/chat#459, #478) | Not exposed |`get linearClient(): LinearClient` (plus deprecated `client` alias) returns the `@linear/sdk``LinearClient`, per-org from `AsyncLocalStorage` in multi-tenant OAuth mode |`@linear/sdk` is TypeScript-only and no official Linear Python SDK exists; the adapter issues GraphQL directly over `aiohttp` (`_graphql_query`) and already documents that stance. Nothing honest to put behind the name. Revisit only if Linear ships an official Python SDK (the Linear `fetch_subject` half of #459 is blocked on the same). |
673
674
|`@chat-adapter/tests` adapter test kit (vercel/chat#470) | Not ported | New TS package with test utilities for adapter authors | Python already ships `chat_sdk.testing` (`MockAdapter`, `MockStateAdapter`, `create_test_message()`) covering the same surface for this repo's adapter tests; mirroring the TS kit verbatim would duplicate it. Revisit if upstream's kit grows capabilities ours lacks (e.g. recorded replay fixtures for third-party adapter authors). |
0 commit comments