hcengineering/platform · warning

Clients disconnected. Closing Workspace...

Error message

Clients disconnected. Closing Workspace...

What it means

Warning logged inside TSessionManager.doCloseAll (guarded by LOGGING_ENABLED) when all client sessions of a workspace are being disconnected as part of closing the workspace. Each session socket gets an upgrade notification (for reasons 'upgrade'/'force-close') and is closed; reconnectIds entries are removed. It marks the point where the workspace stops serving clients.

Source

Thrown at foundations/server/packages/server/src/sessionManager.ts:1224

        reason
      })
    }

    const sessions = Array.from(workspace.sessions)
    workspace.sessions.clear()

    const closeS = (s: Session, webSocket: ConnectionSocket): void => {
      s.workspaceClosed = true
      if (reason === 'upgrade' || reason === 'force-close') {
        // Override message handler, to wait for upgrading response from clients.
        this.sendUpgrade(workspace.context, webSocket, s.binaryMode, s.useCompression)
      }
      webSocket.close()
      this.reconnectIds.delete(s.sessionId)
    }

    if (LOGGING_ENABLED) {
      this.ctx.warn('Clients disconnected. Closing Workspace...', {
        url: workspace.wsId.url,
        uuid: workspace.wsId.uuid
      })
    }

    sessions
      .filter((it) => it[1].socket.id !== ignoreSocket?.id)
      .forEach((s) => {
        closeS(s[1].session, s[1].socket)
      })

    if (reason !== 'upgrade') {
      await workspace.close(this.ctx)
      if (LOGGING_ENABLED) {
        this.ctx.warn('Workspace closed...', { uuid: workspace.wsId.uuid, url: workspace.wsId.url })
      }
    }
  }

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Expected during controlled closes/upgrade — clients reconnect automatically; ensure client code handles close code 99 and the TxModelUpgrade notification.
  2. If unexpected, trace which caller invoked doCloseAll (forceClose vs shutdown vs upgrade) via the preceding 'closing workspace' info log.
  3. During shutdown, use graceful drain windows so in-flight client operations complete before close.
Defensive patterns

Strategy: try-catch

Validate before calling

// before shutdown, drain gracefully
await sessionManager.stopAcceptingConnections()
await waitForInFlightOperations(drainTimeout)

Type guard

function hasActiveSessions(workspace: Workspace): boolean {
  return workspace.sessions.size > 0
}

Try / catch

try {
  await sessionManager.forceClose(wsId)
} catch (err) {
  logger.error('workspace close interrupted', { wsId, err })
  await sessionManager.forceClose(wsId) // retry once to finish teardown
}

Prevention

When it happens

Trigger: doCloseAll(workspace, code, reason, ignoreSocket?) reached its disconnect phase — invoked from forceClose (reason 'force-close'), shutdown (closeWorkspaces), or an upgrade-triggered close. Fires whenever there is at least one iteration of the close path and LOGGING_ENABLED is true.

Common situations: Server shutdown draining workspaces; force-close of a stuck workspace; an upgrade closing sessions so clients switch to the upgraded model; users seeing a mass disconnect/reconnect wave.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/12f7734912a4b470. Report an issue: GitHub.