Maintainer edit for PR 4115: rebase onto origin/main and split gateway HTTP routing from token, media, and workspace services so WebSocketChannel depends on explicit gateway services instead of GatewayHTTPHandler internals.
Preserve file edit channel capabilities and restore tools.restrict_to_workspace wiring through ChannelManager.
Extract all HTTP route handling (bootstrap, sessions, settings, media,
commands, sidebar state, static serving, token management) into a new
GatewayHTTPHandler class in nanobot/channels/ws_http.py.
WebSocketChannel is reduced from 1907 to 1372 lines (-28%), retaining
only WebSocket connection management and message dispatch.
No behavior change. 3730 tests pass, 0 failures.
Shared HTTP utility functions (path parsing, response builders, auth
helpers) now live in ws_http.py with websocket.py importing from there,
avoiding circular dependencies.
Backwards-compat property aliases on WebSocketChannel ensure existing
tests continue to work without modification.
maintainer edit: track background handler tasks, surface failed OneBot actions, reject image redirects, and add focused unit coverage for group routing and edge cases.
maintainer edit: render the Projects divider only before the first project group so Chats can sort between projects without duplicating the heading. Add middle and last ordering regression coverage.
In project-based sidebar grouping, the "Chats" section (non-project
conversations) was always appended at the end regardless of its most
recent updated_at. This meant the newest conversation could appear
below older project groups.
Move Chats group insertion before the global sort, compute its
updatedAt from its most recently updated session, and sort all groups
together by updatedAt descending.
Add a new config option group_user_isolation (default: false) to the
DingTalk channel. When enabled, each user in a group chat gets their own
session while bot replies are still routed to the shared group chat.
The previous fix made retain_recent_legal_suffix return the actual dropped
message list, but already_consolidated was still computed with
min(before_last_consolidated, len(dropped)), which assumes dropped messages
are always a prefix. In the else branch (tail has no user messages), dropped
may include messages from after the consolidated prefix, causing
already_consolidated to skip too many and leaving tail messages neither
retained nor raw-archived.
Fix by having retain_recent_legal_suffix return (dropped,
already_consolidated_count) where already_consolidated_count is computed
against original message indices. Also fix last_consolidated update to count
how many retained messages were inside the old consolidated prefix.
When retain_recent_legal_suffix hits the else branch (tail has no user
messages), it takes a non-contiguous slice from the middle of the session.
enforce_file_cap incorrectly assumed dropped messages were always a prefix
(before[:dropped_count]), causing user messages to be both archived and
retained, and some messages to silently disappear.
Fix by having retain_recent_legal_suffix return the actual dropped message
list using identity-based diff, so enforce_file_cap no longer needs to
guess which messages were removed.