Skip to content

bug(bots): retired WebSocket events mutate stopped or restarted bridges #5947

Description

@jackeyfaker77

What happened

WsBridgeBase ignores close events from a detached socket, but accepts its open and message events. After stop() completes, a delayed open event sets running back to true, and a delayed message still reaches protocol handling.

On the shared Discord/QQ gateway layer, late READY and HELLO frames can also overwrite session/readiness state or restart heartbeat work. After the same bridge starts a replacement socket, the previous socket can still modify the new connection state. The transport base is shared with DingTalk Stream.

Expected: open/message callbacks only act on the current socket while the bridge has not been explicitly stopped. Current-socket events must continue working.

How to reproduce

Build the repository, save the following as ws-retirement-repro.mjs in its root, and run node ws-retirement-repro.mjs. The fake socket makes delayed events deterministic and uses no network or provider credentials.

import { createDefaultBotChannel } from '@maka/core/settings';
import { WsBridgeBase } from './packages/runtime/dist/bots/ws-bridge-base.js';

class FakeSocket extends EventTarget {
  close() {}
  fire(type, data) {
    const event = new Event(type);
    if (data !== undefined) Object.defineProperty(event, 'data', { value: data });
    this.dispatchEvent(event);
  }
}
class FixtureBridge extends WsBridgeBase {
  sockets = [];
  messages = [];
  createWebSocket() {
    const socket = new FakeSocket();
    this.sockets.push(socket);
    return socket;
  }
  async openConnection() { this.connect('wss://fixture.invalid'); }
  checkCredentials() { return null; }
  decideClose() { return { kind: 'stopped' }; }
  handleWsMessage(data) { this.messages.push(data); }
}
const bridge = new FixtureBridge('discord', {
  ...createDefaultBotChannel('discord'), enabled: true, token: 'fixture-token',
});
await bridge.start();
const retired = bridge.sockets[0];
await bridge.stop();
const runningImmediatelyAfterStop = bridge.isRunning();
retired.fire('open');
retired.fire('message', 'late payload');
console.log(JSON.stringify({
  runningImmediatelyAfterStop,
  runningAfterLateOpen: bridge.isRunning(),
  lateMessagesProcessed: bridge.messages.length,
}));

Actual output on the affected upstream revision:

{"runningImmediatelyAfterStop":false,"runningAfterLateOpen":true,"lateMessagesProcessed":1}

Expected:

{"runningImmediatelyAfterStop":false,"runningAfterLateOpen":false,"lateMessagesProcessed":0}

Environment

  • Maka commit: ab5996b (upstream main)
  • OS: Windows, x64
  • Surface: shared outbound WebSocket Bot transport in packages/runtime
  • Node.js: v24.20.0
  • Fake-socket reproduction; no live provider account or SDK end-to-end test.

Logs, screenshots, or additional context

The close callback already checks this.ws !== ws; open and message lack the corresponding lifecycle fence. I am preparing a two-line guard fix and three deterministic regressions for post-stop open, post-stop messages/heartbeats, and old-vs-current sockets after restart.

Related: #5945 / #5946 address retired bridge subscriptions in BotRegistry. This issue concerns a retired socket inside a bridge, including a bridge that remains current during reconnect/restart, so it is an independent transport-layer fix.

Automated report prepared with OpenAI Codex on behalf of the contributor.

Activity

  1. jackeyfaker77 commented on Oct 2, 2026

    @jackeyfaker77
    ContributorAuthor

    take

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions