qwen-code/docs/users/features/channels/overview.md
曹潇缤 8fcc43943d
feat(channel): add QQ Bot (QQ机器人) channel adapter (#5202)
* feat(channel): add QQ Bot channel adapter

Add @qwen-code/channel-qqbot package implementing QQ Bot WebSocket
Gateway connection via the official QQ Bot API.

Supports:
- WebSocket Gateway (HELLO/IDENTIFY/HEARTBEAT/DISPATCH/RECONNECT)
- C2C single chat (C2C_MESSAGE_CREATE)
- Group @mention (GROUP_AT_MESSAGE_CREATE) — code path exists, unverified
- Streaming output via msg_id + msg_seq multi-block sending
- Auto-reconnect with exponential backoff
- Sandbox environment toggle

TODO (technical debt acknowledged):
- Group chat not verified end-to-end
- Single-file architecture (should split into gateway/send/auth modules
  like weixin channel)
- No tests (weixin has send.test.ts + media.test.ts)
- No typing indicator (onPromptStart/onPromptEnd not yet implemented)
- No channel instructions injection in connect()
- No structured error types

Closes #5201

* feat(qqbot): add QR login, group chat support with typed events

- Add QR code login via @tencent-connect/qqbot-connector with credential persistence
- Add Intent constants for C2C (1<<12) and GROUP_AT_MESSAGE (1<<25)
- Use QQGroupMessageEvent type in handleGroup instead of cast
- Remove resolved TODO comments for group chat verification
- Add msg_seq to send error log for debugging

* fix(qqbot): address PR review — lint errors, token refresh, security

- Use bracket notation for Record<string, unknown> to fix TS4111 lint errors
- Add chmodSync(credsFile, 0o600) for credential file permissions
- Implement token refresh at 80% TTL with expires_in tracking
- Fix RECONNECT opcode: use code 4000 + serverRequestedReconnect flag
- Fix connect() Promise: reject on close before READY via connectReject
- Log empty-token case in sendMessage, drain response body on error
- Clear chatTypeMap/replyMsgId/msgSeqMap in disconnect()
- Capture msgId at send-time to avoid race on replyMsgId
- Switch channel-registry.ts to Promise.allSettled (isolated channel failures)
- Add chatId validation (isValidChatId) to prevent SSRF

* fix(qqbot): add qqbot to build order, fix ESLint default-case

- Add packages/channels/qqbot to scripts/build.js buildOrder
  (CLI imports @qwen-code/channel-qqbot but it wasn't being built)
- Add default case to handleGatewayMessage switch

* feat(qqbot): prepend sender name in group messages for shared context

When sessionScope is set to 'thread', all group members share one
session. Prepending [senderName] helps the agent distinguish who
said what in the shared context.

* feat(qqbot): cross-server context continuation via SessionRouter persistence

- Persist SessionRouter mappings to disk via sessionsPath, surviving daemon restarts
- Persist QQ routing state (chatTypeMap, replyMsgId, msgSeqMap) to {name}-state.json
- Backup/restore global sessions.json on disconnect/connect to survive start.ts cleanup
- fixRestoredSessions() workaround for ACP LoadSessionResponse missing sessionId
- READY handler delays resolve() until restoreSessions() completes, preventing race

* feat(qqbot): add Session Resume + reconnect retry resilience

- Support WS session resume (RESUME opcode 6) on reconnect,
  falling back to full IDENTIFY when session is invalid
- Add reconnectWithRetry() loop: retries gateway fetch up to 5x
  with exponential backoff, then schedules 60s fallback retry
  (fixes silent death after GW HTTP 500)
- connect() now retries up to 3 times on initial failure
- Bump maxReconnectAttempts from 10 to 20
- Refresh token before each reconnect attempt

* fix(qqbot): address review feedback from wenshao

- fixRestoredSessions: use entry.target directly instead of tt.get(undefined)
  (fixes first restored session routing to wrong conversation when 2+ sessions)
- scheduleTokenRefresh: retry in 60s on token refresh failure, not just log
- sendMessage: move saveQQState() after chunk loop, avoid redundant disk I/O
- handleGroup: drop message when group_openid is missing instead of falling
  back to author.id (which would cause 404 on group message send)

* fix(qqbot): address 3rd review from doudouOUC (12 issues)

- QWEN_HOME: use getGlobalQwenDir() instead of homedir()
- name sanitization: prevent path traversal in file paths
- fetch timeouts: AbortSignal.timeout(15s) on all 3 fetch calls
- TOCTOU: writeFileSync with {mode: 0o600} instead of chmodSync after
- msg_seq gaps: only increment seq on send success, break on failure
- message dedup: seenMessages Map with 5min TTL cleanup timer
- disconnect: set disposed flag + flushQQState sync + clear timers
- heartbeat ACK: track lastHeartbeatAck, force close on 2x interval timeout
- reconnect exhaustion: FATAL log when max attempts reached post-connect
- debounced saveQQState: 500ms debounce, flush on disconnect
- handleGroup: skip [senderName] prefix for slash commands, log for audit
- disposed guard: connectGateway checks disposed before creating WS

* fix(qqbot): robustness round — RESUMED, token expiry, SSRF, disposed, typing stubs

- Handle RESUMED event on RESUME success (start heartbeat, restore sessions)
- Check token expiry before sendMessage, refresh if expired
- Tighten isValidChatId regex (remove . and /) to close path traversal
- Reset disposed flag in connect() for reusability
- Add onPromptStart/onPromptEnd stubs (QQ Bot has no typing API)
- Add robustness comments for splitText surrogate pairs, restoreQQState
  corruption, and senderId identity fragmentation across contexts

* refactor(qqbot): split into modules — api, accounts, login

Extract HTTP calls, credential I/O, and QR login into separate files
matching the weixin channel's architecture:

- api.ts: fetchAccessToken, fetchGatewayUrl, getApiBase, sendQQMessage
- accounts.ts: getCredsFilePath, loadCredentials, saveCredentials
- login.ts: qrCodeLogin (qrConnect wrapper)

QQChannel.ts drops inline fetch/credential/qrConnect logic and imports
from the new modules. Net -41 lines in the adapter.

* feat(qqbot): markdown message support (msg_type: 2)

Detect markdown syntax in AI responses and send as msg_type=2
with markdown.content field instead of plain-text msg_type=0.

Detection covers headers, code blocks, bold, italic, strikethrough,
inline code, links, and lists via a single regex.

* fix(qqbot): defensive patches from complete review

- reconnectWithRetry: guard against disposed channel to prevent infinite loop
- handleGroup: broaden @mention regex to match both legacy <@!id> and V2 <@openid>
- handleGroup: set isReplyToBot=true (every group msg is an @mention)
- fixRestoredSessions: document fragile private-field access
- saveCredentials: correct TOCTOU claim in comment
- hasMarkdownSyntax: document false-positive trade-off

* fix(qqbot): guard against empty content in C2C and group handlers

- handleC2C: return early when event.content is null/empty (image/sticker msgs)
- handleGroup: return early when cleanText is empty after @mention stripping

* fix(qqbot): close remaining review gaps — disposed guard, connectReject, token retry, RESUMED restore

* fix(qqbot): address wenshao review — RESUME restore removal, disposed guards, timer tracking, logging, heartbeat floor, requiredConfigFields, channel-registry error labels

* fix(qqbot): markdown fallback to plain text on rejection

* docs(qqbot): clarify markdown permission — Open Platform has no gate, FAQ is a different platform

* feat(qqbot): add Ark (msg_type=3) and Media (msg_type=7) message support

- types.ts: ArkKV, ArkPayload, FileType, MediaUploadRequest/Response, MediaPayload
- api.ts: uploadQQMedia() — file upload for rich media
- QQChannel.ts: sendArk(chatId, templateId, kv) + sendMedia(chatId, fileType, url, text?)
  - C2C/group upload paths separated (file_info not interchangeable)
  - file_type=4 (文件) blocked for groups per QQ API
  - Embed (msg_type=4) skipped — QQ频道专用, not available for Bot Open Platform

* feat(qqbot): auto-route !ark / !media commands from LLM text via sendMessage

LLM outputs text — the channel now parses structured commands inline:
  !ark(24, #TITLE#=标题, #META_DESC#=描述)
  !media(image, https://example.com/photo.jpg, caption text)

parseArkCommand / parseMediaCommand extract at sendMessage entry;
normal text/markdown flow unchanged.

* feat(qqbot): inject channel instructions for ark/media commands

Sets config.instructions on connect() so the LLM learns about:
  !ark(template_id, key=val, ...) — 3 default templates (23/24/37)
  !media(type, url, [caption])   — image/video/voice/file

Fixes known debt: 'No channel instructions'.

* feat(qqbot): gate ark/media behind config flags (enableArk/enableMedia)

Both features default to false — opt-in via settings.json:
  channels.my-qq.enableArk = true
  channels.my-qq.enableMedia = true

Instructions injected conditionally; command routing gated per-flag.

* refactor(qqbot): extract resolveRoute() to eliminate duplication across sendMessage/sendArk/sendMedia

disposed check, token refresh, chatId validation, sandbox path selection
now in one place. All three methods call resolveRoute() instead of
repeating the same 15-line preamble.

* chore(qqbot): remove Ark and Media message support

Remove !ark() / !media() text parsing, sendArk/sendMedia methods,
uploadQQMedia, and all related types. The text-parsing approach
was too fragile against LLM output formatting. Only text/markdown
messaging remains.

* fix(qqbot): robustness patches for review findings

- Add { mode: 0o600 } to all writeFileSync calls (state/session files)
- Guard against stale WebSocket close event nuking new connection
- Add isReconnecting guard to prevent parallel reconnectWithRetry chains
- Reset isReconnecting flag in READY, RESUMED, and exhaustion paths

* docs(channel): add QQ Bot user documentation

Add user-facing documentation for the QQ Bot channel adapter:

- New docs/users/features/channels/qqbot.md covering setup, configuration,
  QR code login, group chat, Markdown support, token management, connection
  resilience, and troubleshooting
- Update docs/users/features/channels/_meta.ts to include QQ Bot in nav
- Update docs/users/features/channels/overview.md to reference QQ Bot
  across the intro, quick start, type options, slash commands, and the
  media platform differences table

* docs(qqbot): fix prerequisites — QR login needs no developer account

QR code login via qrConnect() does not require a developer account or
manual app registration. First qwen channel start is all you need.

* docs(qqbot): emphasize QR login, keep developer portal as secondary path

Both paths work (config → persisted file → QR scan), confirmed against
fetchToken() code. Reposition QR code login as the primary setup flow,
remove redundant tips/troubleshooting entries.

* docs(qqbot): remove Images and Files section — not supported in channel code

handleC2C/handleGroup both skip messages with no text content.
No media download or upload logic exists in this channel adapter.

* test(qqbot): add unit tests for send utilities

Add vitest test suite for QQ Bot channel following the weixin channel
testing patterns. Extract isValidChatId, hasMarkdownSyntax, and splitText
as exported module-level functions to enable direct testing.

- 27 tests covering: chatId SSRF validation, Markdown syntax detection, and
  text chunking for QQ's 2000-char message limit
- Add vitest.config.ts and test script to qqbot package
- Register qqbot in root vitest workspace projects

Refs: #5202

* test(qqbot): add sendMessage flow tests with mocked API

Follow the weixin sendImage test pattern: mock sendQQMessage and
channel-base dependencies to test sendMessage end-to-end.

- C2C/group routing verification
- Markdown msg_type=2 vs plain text msg_type=0
- Markdown rejection fallback to plain text
- Disposed guard and error-stop behavior
- msg_id + msg_seq tracking for multi-chunk streaming

9 new tests, 36 total (all passing)

* test(qqbot): fix review issues — add missing edge cases

Self-review fixes:
- Fix misleading test name: 'returns early when chatId not in chatTypeMap'
  → 'defaults to C2C path for unknown chatId' (code doesn't return early)
- Add SSRF validation test: sendMessage rejects '../traversal' chatId
- Add network error test: thrown sendQQMessage caught by try/catch
- Add token expiration test: expired token + failed refresh → early return
- Hoist mockFetchAccessToken and set default resolved value in beforeEach
  to prevent silent undefined-access failures in accidental token-refresh paths

39 tests, all passing

* test(qqbot): add api and accounts unit tests

Add api.test.ts (13 tests) and accounts.test.ts (8 tests) following
weixin channel vitest patterns: vi.hoisted() mocks, vi.mock() module
replacement, and dynamic import() after mock setup.

api.test.ts covers getApiBase, sendQQMessage, fetchAccessToken, and
fetchGatewayUrl — including HTTP errors, missing fields, and request
body format.

accounts.test.ts covers getCredsFilePath, loadCredentials (missing file,
corrupt JSON, missing fields, valid data), and saveCredentials (dir
creation + 0o600 permissions).

All 60 tests pass (39 existing + 21 new). tsc --build and eslint clean.

* chore(qqbot): suppress CodeQL ReDoS false positives

Add codeql[js/polynomial-redos] suppression comments for two
regexes flagged by CodeQL:

- hasMarkdownSyntax(): input is LLM-generated reply text,
  never attacker-controlled in Qwen Code Channel context.
- handleGroup(): <@...> prefix is injected by QQ servers;
  openid is assigned by QQ, not attacker-chosen.

Both paths have no practical exploit vector — an adversary
would need to either control an LLM's output or register a
malicious openid with QQ, neither of which is achievable.

* fix(qqbot): allow QR-code-only login and guard qrConnect return

- requiredConfigFields: [] — fetchToken() already resolves credentials
  from config → persisted file → QR fallback chain. Blocking at config
  validation prevented QR-code-only users from starting the channel.
- qrCodeLogin(): add bounds check for empty qrConnect() return value.
  If the external library returns an empty array, throw descriptive
  error instead of crashing with TypeError on creds.appId.

* chore(qqbot): add comments for requiredConfigFields and qrConnect guard

- index.ts: explain why requiredConfigFields is empty — fetchToken()
  already resolves credentials via config → file → QR fallback chain.
  Requiring appID/appSecret at config level would block QR-only users
  from reaching the fallback through the built-in channel path.

- login.ts: clarify qrConnect() guard is a defensive robustness patch,
  not a response to an observed failure. Verified by removing appID
  from config and running qwen channel start — QR login triggers
  correctly and returns valid credentials.

* fix(qqbot): replace quadratic regexes with linear patterns, remove failed suppress comments

* fix(qqbot): split hasMarkdownSyntax into individual tests to pass CodeQL

* fix(qqbot): replace markdown link regex with indexOf to eliminate CodeQL ReDoS
2026-06-19 06:32:52 +08:00

15 KiB

Channels

Channels let you interact with a Qwen Code agent from messaging platforms like Telegram, WeChat, QQ, or DingTalk, instead of the terminal. You send messages from your phone or desktop chat app, and the agent responds just like it would in the CLI.

How It Works

When you run qwen channel start, Qwen Code:

  1. Reads channel configurations from your settings.json
  2. Spawns a single agent process using the Agent Client Protocol (ACP)
  3. Connects to each messaging platform and starts listening for messages
  4. Routes incoming messages to the agent and sends responses back to the correct chat

All channels share one agent process with isolated sessions per user. Each channel can have its own working directory, model, and instructions.

Quick Start

  1. Set up a bot on your messaging platform (see channel-specific guides: Telegram, WeChat, QQ Bot, DingTalk)
  2. Add the channel configuration to ~/.qwen/settings.json
  3. Run qwen channel start to start all channels, or qwen channel start <name> for a single channel

Want to connect a platform that isn't built in? See Plugins to add a custom adapter as an extension.

Configuration

Channels are configured under the channels key in settings.json. Each channel has a name and a set of options:

{
  "channels": {
    "my-channel": {
      "type": "telegram",
      "token": "$MY_BOT_TOKEN",
      "senderPolicy": "allowlist",
      "allowedUsers": ["123456789"],
      "sessionScope": "user",
      "cwd": "/path/to/working/directory",
      "instructions": "Optional system instructions for the agent.",
      "groupPolicy": "disabled",
      "groups": {
        "*": { "requireMention": true }
      }
    }
  }
}

Options

Option Required Description
type Yes Channel type: telegram, weixin, qq, dingtalk, feishu, or a custom type from an extension (see Plugins)
token Telegram Bot token. Supports $ENV_VAR syntax to read from environment variables. Not needed for WeChat or DingTalk
clientId DingTalk DingTalk AppKey. Supports $ENV_VAR syntax
clientSecret DingTalk DingTalk AppSecret. Supports $ENV_VAR syntax
model No Model to use for this channel (e.g., qwen3.5-plus). Overrides the default model. Useful for multimodal models that support image input
senderPolicy No Who can talk to the bot: allowlist (default), open, or pairing
allowedUsers No List of user IDs allowed to use the bot (used by allowlist and pairing policies)
sessionScope No How sessions are scoped: user (default), thread, or single
cwd No Working directory for the agent. Defaults to the current directory
instructions No Custom instructions prepended to the first message of each session
groupPolicy No Group chat access: disabled (default), allowlist, or open. See Group Chats
groups No Per-group settings. Keys are group chat IDs or "*" for defaults. See Group Chats
dispatchMode No What happens when you send a message while the bot is busy: steer (default), collect, or followup. See Dispatch Modes
blockStreaming No Progressive response delivery: on or off (default). See Block Streaming
blockStreamingChunk No Chunk size bounds: { "minChars": 400, "maxChars": 1000 }. See Block Streaming
blockStreamingCoalesce No Idle flush: { "idleMs": 1500 }. See Block Streaming

Sender Policy

Controls who can interact with the bot:

  • allowlist (default) — Only users listed in allowedUsers can send messages. Others are silently ignored.
  • pairing — Unknown senders receive a pairing code. The bot operator approves them via CLI, and they're added to a persistent allowlist. Users in allowedUsers skip pairing entirely. See DM Pairing below.
  • open — Anyone can send messages. Use with caution.

Session Scope

Controls how conversation sessions are managed:

  • user (default) — One session per user. All messages from the same user share a conversation.
  • thread — One session per thread/topic. Useful for group chats with threads.
  • single — One shared session for all users. Everyone shares the same conversation.

Token Security

Bot tokens should not be stored directly in settings.json. Instead, use environment variable references:

{
  "token": "$TELEGRAM_BOT_TOKEN"
}

Set the actual token in your shell environment or in a .env file that gets loaded before running the channel.

DM Pairing

When senderPolicy is set to "pairing", unknown senders go through an approval flow:

  1. An unknown user sends a message to the bot
  2. The bot replies with an 8-character pairing code (e.g., VEQDDWXJ)
  3. The user shares the code with you (the bot operator)
  4. You approve them via CLI:
qwen channel pairing approve my-channel VEQDDWXJ

Once approved, the user's ID is saved to ~/.qwen/channels/<name>-allowlist.json and all future messages go through normally.

Pairing CLI Commands

# List pending pairing requests
qwen channel pairing list my-channel

# Approve a request by code
qwen channel pairing approve my-channel <CODE>

Pairing Rules

  • Codes are 8 characters, uppercase, using an unambiguous alphabet (no 0/O/1/I)
  • Codes expire after 1 hour
  • Maximum 3 pending requests per channel at a time — additional requests are ignored until one expires or is approved
  • Users listed in allowedUsers in settings.json always skip pairing
  • Approved users are stored in ~/.qwen/channels/<name>-allowlist.json — treat this file as sensitive

Group Chats

By default, the bot only works in direct messages. To enable group chat support, set groupPolicy to "allowlist" or "open".

Group Policy

Controls whether the bot participates in group chats at all:

  • disabled (default) — The bot ignores all group messages. Safest option.
  • allowlist — The bot only responds in groups explicitly listed in groups by chat ID. The "*" key provides default settings but does not act as a wildcard allow.
  • open — The bot responds in all groups it's added to. Use with caution.

Mention Gating

In groups, the bot requires an @mention or a reply to one of its messages by default. This prevents the bot from responding to every message in a group chat.

Configure per-group with the groups setting:

{
  "groups": {
    "*": { "requireMention": true },
    "-100123456": { "requireMention": false }
  }
}
  • "*" — Default settings for all groups. Only sets config defaults, not an allowlist entry.
  • Group chat ID — Override settings for a specific group. Overrides "*" defaults.
  • requireMention (default: true) — When true, the bot only responds to messages that @mention it or reply to one of its messages. When false, the bot responds to all messages (useful for dedicated task groups).

How group messages are evaluated

1. groupPolicy — is this group allowed?           (no → ignore)
2. requireMention — was the bot mentioned/replied to? (no → ignore)
3. senderPolicy — is this sender approved?         (no → pairing flow)
4. Route to session

Telegram Setup for Groups

  1. Add the bot to a group
  2. Disable privacy mode in BotFather (/mybots → Bot Settings → Group Privacy → Turn Off) — otherwise the bot won't see non-command messages
  3. Remove and re-add the bot to the group after changing privacy mode (Telegram caches this setting)

Finding a Group Chat ID

To find a group's chat ID for the groups allowlist:

  1. Stop the bot if it's running
  2. Send a message mentioning the bot in the group
  3. Use the Telegram Bot API to check queued updates:
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates" | python3 -m json.tool

Look for message.chat.id in the response — group IDs are negative numbers (e.g., -5170296765).

Media Support

Channels support sending images and files to the agent, not just text.

Images

Send a photo to the bot and the agent will see it — useful for sharing screenshots, error messages, or diagrams. The image is sent directly to the model as a vision input.

To use image support, configure a multimodal model for the channel:

{
  "channels": {
    "my-channel": {
      "type": "telegram",
      "model": "qwen3.5-plus",
      ...
    }
  }
}

Files

Send a document (PDF, code file, text file, etc.) to the bot. The file is downloaded and saved to a temporary directory, and the agent is told the file path so it can read the contents using its file-reading tools.

Files work with any model — no multimodal support required.

Platform differences

Feature Telegram WeChat DingTalk
Images Direct download via Bot API CDN download with AES decryption downloadCode API (two-step)
Files Direct download via Bot API (20MB limit) CDN download with AES decryption downloadCode API (two-step)
Captions Photo/file captions included as message text Not applicable Rich text: mixed text + images in one message

Dispatch Modes

Controls what happens when you send a new message while the bot is still processing a previous one.

  • steer (default) — The bot cancels the current request and starts working on your new message. Best for normal chat, where a follow-up usually means you want to correct or redirect the bot.
  • collect — Your new messages are buffered. When the current request finishes, all buffered messages are combined into a single follow-up prompt. Good for async workflows where you want to queue up thoughts.
  • followup — Each message is queued and processed as its own separate turn, in order. Useful for batch workflows where each message is independent.
{
  "channels": {
    "my-channel": {
      "type": "telegram",
      "dispatchMode": "steer",
      ...
    }
  }
}

You can also set dispatch mode per group, overriding the channel default:

{
  "groups": {
    "*": { "requireMention": true, "dispatchMode": "steer" },
    "-100123456": { "dispatchMode": "collect" }
  }
}

Block Streaming

By default, the agent works for a while and then sends one large response. With block streaming enabled, the response arrives as multiple shorter messages while the agent is still working — similar to how ChatGPT or Claude show progressive output.

{
  "channels": {
    "my-channel": {
      "type": "telegram",
      "blockStreaming": "on",
      "blockStreamingChunk": { "minChars": 400, "maxChars": 1000 },
      "blockStreamingCoalesce": { "idleMs": 1500 },
      ...
    }
  }
}

How it works

  • The agent's response is split into blocks at paragraph boundaries and sent as separate messages
  • minChars (default 400) — don't send a block until it's at least this long, to avoid spamming tiny messages
  • maxChars (default 1000) — if a block gets this long without a natural break, send it anyway
  • idleMs (default 1500) — if the agent pauses (e.g., running a tool), send what's buffered so far
  • When the agent finishes, any remaining text is sent immediately

Only blockStreaming is required. The chunk and coalesce settings are optional and have sensible defaults.

Slash Commands

Channels support slash commands. These are handled locally (no agent round-trip):

  • /help — List available commands
  • /clear — Clear your session and start fresh (aliases: /reset, /new)
  • /status — Show session info and access policy

All other slash commands (e.g., /compress, /summary) are forwarded to the agent.

These commands work on all channel types (Telegram, WeChat, QQ, DingTalk).

Running

# Start all configured channels (shared agent process)
qwen channel start

# Start a single channel
qwen channel start my-channel

# Check if the service is running
qwen channel status

# Stop the running service
qwen channel stop

The bot runs in the foreground. Press Ctrl+C to stop, or use qwen channel stop from another terminal.

Multi-Channel Mode

When you run qwen channel start without a name, all channels defined in settings.json start together sharing a single agent process. Each channel maintains its own sessions — a Telegram user and a WeChat user get separate conversations, even though they share the same agent.

Each channel uses its own cwd from its config, so different channels can work on different projects simultaneously.

Service Management

The channel service uses a PID file (~/.qwen/channels/service.pid) to track the running instance:

  • Duplicate prevention: Running qwen channel start while a service is already running will show an error instead of starting a second instance
  • qwen channel stop: Gracefully stops the running service from another terminal
  • qwen channel status: Shows whether the service is running, its uptime, and session counts per channel

Crash Recovery

If the agent process crashes unexpectedly, the channel service automatically restarts it and attempts to restore all active sessions. Users can continue their conversations without starting over.

  • Sessions are persisted to ~/.qwen/channels/sessions.json while the service is running
  • On crash: the agent restarts within 3 seconds and reloads saved sessions
  • After 3 consecutive crashes, the service exits with an error
  • On clean shutdown (Ctrl+C or qwen channel stop): session data is cleared — the next start is always fresh