claude-threads 1.31.2 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.32.0] - 2026-09-01
11
+
12
+ ### Added
13
+ - **Slack: shared event source — one Socket Mode connection per app** (#502, thanks @kaza). Slack round-robins Socket Mode envelopes across all of an app's open connections, so a second `SlackClient` on the same app token silently steals events from the first. Now exactly one client (the parent) owns the socket; other clients register as secondaries and receive their channels' events injected by the parent — Web API calls stay independent per instance. The parent mirrors connection state onto secondaries (idempotently re-armed across disconnects), and on reconnect, missed-message recovery runs for the parent and every registered secondary. Zero behavior change for existing single-channel configs; this is the mechanism that unblocks DM auto-discovery on Slack and other multi-channel consumers.
14
+
10
15
  ## [1.31.2] - 2026-08-30
11
16
 
12
17
  ### Added
package/dist/index.js CHANGED
@@ -71941,8 +71941,13 @@ class SlackClient extends BasePlatformClient {
71941
71941
  rateLimitRetryAfter = 0;
71942
71942
  outboundFiles;
71943
71943
  formatter = new SlackFormatter;
71944
- constructor(platformConfig) {
71944
+ channelClients = new Map;
71945
+ sharedEventSource;
71946
+ socketConnected = false;
71947
+ constructor(platformConfig, sharedEventSource) {
71945
71948
  super();
71949
+ this.sharedEventSource = sharedEventSource;
71950
+ this.installStateMirror();
71946
71951
  this.platformId = platformConfig.id;
71947
71952
  this.displayName = platformConfig.displayName;
71948
71953
  this.botToken = platformConfig.botToken;
@@ -71956,6 +71961,55 @@ class SlackClient extends BasePlatformClient {
71956
71961
  this.approvals = platformConfig.approvals;
71957
71962
  this.ackReaction = normalizeAckReaction(platformConfig.ackReaction, `platforms[${platformConfig.id}].ackReaction`);
71958
71963
  }
71964
+ stateMirrors = ["connected", "disconnected", "reconnecting"].map((state) => ({
71965
+ state,
71966
+ handler: (...args) => {
71967
+ for (const secondary of this.channelClients.values()) {
71968
+ secondary.emit(state, ...args);
71969
+ }
71970
+ }
71971
+ }));
71972
+ installStateMirror() {
71973
+ for (const { state, handler: handler2 } of this.stateMirrors) {
71974
+ this.off(state, handler2);
71975
+ this.on(state, handler2);
71976
+ }
71977
+ }
71978
+ onConnectionEstablished() {
71979
+ const wasReconnecting = this.isReconnecting;
71980
+ super.onConnectionEstablished();
71981
+ if (!wasReconnecting)
71982
+ return;
71983
+ for (const secondary of this.channelClients.values()) {
71984
+ secondary.recoverMissedMessages().catch((err) => {
71985
+ log40.warn(`Failed to recover missed messages for ${secondary.platformId}: ${err}`);
71986
+ });
71987
+ }
71988
+ }
71989
+ registerChannelClient(channelId, client) {
71990
+ if (channelId === this.channelId) {
71991
+ throw new Error(`registerChannelClient: ${channelId} is the parent's own channel — a secondary there can never receive events`);
71992
+ }
71993
+ this.channelClients.set(channelId, client);
71994
+ }
71995
+ unregisterChannelClient(channelId, client) {
71996
+ if (client && this.channelClients.get(channelId) !== client)
71997
+ return;
71998
+ this.channelClients.delete(channelId);
71999
+ }
72000
+ _injectSlackEvent(event) {
72001
+ this.handleSlackEvent(event);
72002
+ }
72003
+ disconnect() {
72004
+ if (this.sharedEventSource) {
72005
+ this.sharedEventSource.unregisterChannelClient(this.channelId, this);
72006
+ }
72007
+ try {
72008
+ return super.disconnect();
72009
+ } finally {
72010
+ this.installStateMirror();
72011
+ }
72012
+ }
71959
72013
  normalizePlatformUser(slackUser) {
71960
72014
  const displayName = slackUser.profile?.display_name || slackUser.profile?.real_name || slackUser.real_name || slackUser.name;
71961
72015
  return {
@@ -72059,6 +72113,16 @@ class SlackClient extends BasePlatformClient {
72059
72113
  return data;
72060
72114
  }
72061
72115
  async connect() {
72116
+ if (this.sharedEventSource) {
72117
+ await this.fetchBotUser();
72118
+ if (this.isIntentionalDisconnect)
72119
+ return;
72120
+ this.sharedEventSource.registerChannelClient(this.channelId, this);
72121
+ if (this.sharedEventSource.socketConnected) {
72122
+ this.emit("connected");
72123
+ }
72124
+ return;
72125
+ }
72062
72126
  await this.fetchBotUser();
72063
72127
  wsLogger.debug(`Slack bot user ID: ${this.botUserId}`);
72064
72128
  const response = await this.appApi("POST", "apps.connections.open");
@@ -72098,12 +72162,8 @@ class SlackClient extends BasePlatformClient {
72098
72162
  this.handleSocketModeEvent(envelope);
72099
72163
  if (envelope.type === "hello") {
72100
72164
  clearTimeout(connectionTimeout);
72165
+ this.socketConnected = true;
72101
72166
  this.onConnectionEstablished();
72102
- if (this.isReconnecting && this.lastProcessedTs) {
72103
- this.recoverMissedMessages().catch((err) => {
72104
- log40.warn(`Failed to recover missed messages: ${err}`);
72105
- });
72106
- }
72107
72167
  doResolve();
72108
72168
  }
72109
72169
  } catch (err) {
@@ -72111,6 +72171,7 @@ class SlackClient extends BasePlatformClient {
72111
72171
  }
72112
72172
  };
72113
72173
  this.ws.onclose = (event) => {
72174
+ this.socketConnected = false;
72114
72175
  clearTimeout(connectionTimeout);
72115
72176
  wsLogger.info(`Socket Mode: WebSocket disconnected (code: ${event.code}, reason: ${event.reason || "none"}, clean: ${event.wasClean})`);
72116
72177
  if (!settled) {
@@ -72162,6 +72223,14 @@ class SlackClient extends BasePlatformClient {
72162
72223
  }
72163
72224
  }
72164
72225
  handleSlackEvent(event) {
72226
+ const eventChannel = event.channel || event.item?.channel;
72227
+ if (eventChannel && eventChannel !== this.channelId) {
72228
+ const secondary = this.channelClients.get(eventChannel);
72229
+ if (secondary) {
72230
+ secondary._injectSlackEvent(event);
72231
+ return;
72232
+ }
72233
+ }
72165
72234
  if (event.type === "message" && (!event.subtype || event.subtype === "file_share")) {
72166
72235
  if (event.user === this.botUserId || event.bot_id) {
72167
72236
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.31.2",
3
+ "version": "1.32.0",
4
4
  "description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",