Commit graph

134 commits

Author SHA1 Message Date
qqqys
b8b3287308
fix(channels): add chat payload diagnostics (#6539)
* fix(channels): improve wecom mention diagnostics

* fix(channels): harden diagnostic logging

* fix(channels): redact platform sender identities

* test(channels): assert debug silence before restoring spies

* fix(channels): log pairing-required preflight drops

* fix(channels): compact debug payload logs

* test(channels): expect compact debug payload logs
2026-07-08 23:23:47 +00:00
qwen-code-ci-bot
b330ec884f
chore(release): v0.19.8 (#6549)
* chore(release): v0.19.8

* docs(changelog): sync for v0.19.8

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-08 15:51:03 +00:00
ChengHui Chen
1f92787aa0
feat(channels): add dmPolicy config to disable private/DM messages (#6521)
* feat(channels): add dmPolicy config to disable private/DM messages

Add DmGate class mirroring GroupGate to gate DM/private messages in
channel adapters. Operators can now set dmPolicy: 'disabled' in their
channel config to silently drop all DM messages while keeping group
messages active.

Closes #6392

* fix(channels): address review feedback for dmPolicy

- Add dmPolicy: 'open' to all test config factories (8 files) to
  maintain type correctness with required ChannelConfig field
- Add integration tests in ChannelBase.test.ts:
  - preflightInbound: DM dropped + group passes when dmPolicy=disabled
  - isStoredLoopTargetAuthorized: DM loop job disabled + group passes
- Add dmPolicy assertions in config-utils.test.ts (default + explicit)
- Keep dmPolicy as required field (not optional) for strict parity
  with groupPolicy
2026-07-08 12:01:10 +00:00
qqqys
58e51eb96c
fix(channel): Relay ACP permission requests (#6446)
* fix(channel): Relay ACP permission requests

* fix(channel): harden permission relay cleanup

* fix(channel): scope ACP permission approvals

* fix(channel): harden permission cancellation diagnostics

* test(channel): cover permission lookup edge cases

* fix(channel): close permission relay stale requests

* fix(channel): tighten approve-always option matching

* test(channel): cover permission relay cleanup gaps

* fix(channel): deliver threaded permission prompts

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-08 03:27:25 +00:00
qwen-code-ci-bot
86ae16a6d6
chore(release): v0.19.7 (#6484)
* chore(release): v0.19.7

* docs(changelog): sync for v0.19.7

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-07 17:25:48 +00:00
qqqys
e3d7d10d1d
[codex] add natural channel memory intents (#6376)
* feat(channels): add natural channel memory intents

* fix(channels): add explicit guard and exhaustiveness check for clear_confirm intent

The clear_confirm path was handled as implicit fall-through at the bottom
of handleChannelMemoryIntent. If a new intent kind were added to the
ChannelMemoryIntent union, it would silently execute clearChannelMemory
without user confirmation — a data-loss risk.

Add explicit if (intent.kind === 'clear_confirm') guard and a
const _exhaustive: never assertion so TypeScript flags any unhandled
kinds at compile time.

* fix(channels): close session leak in classifier and fix regex separator

- BridgeChannelMemoryIntentClassifier now wraps prompt() in try/finally
  to always call cancelSession(), preventing daemon session leaks on
  every classifier invocation. Cleanup errors are caught so they cannot
  mask a successful classification result.
- Add missing optional punctuation separator to the 以后记住 regex
  pattern for consistency with other Chinese remember patterns.

* fix(channels): enforce pending clear state for channel memory confirmation

The clear_confirm intent executed clearChannelMemory directly without
verifying a prior clear_request was issued for the same chat. Any
authorized user could clear any chat's memory by sending the confirmation
phrase standalone, bypassing the two-step flow.

Add a per-target pending clear map (chatId + threadId, 60s TTL) that
is set during clear_request and verified+consumed during clear_confirm.
Standalone confirmation phrases now get rejected with a prompt to
issue the clear request first.

* fix(channels): include senderId in pendingClears key to prevent cross-user confirmation

User A could initiate clear_request in a group chat and User B could
confirm it, since the pending key only included chatId+threadId.
Add senderId to the key so only the user who initiated the clear
can confirm it.

* fix(channels): harden memory intent review fixes

* fix(channels): cover memory clear sender guard

* fix(channels): block group memory mutations

* fix(channels): avoid ambiguous memory saves

* test(channels): cover memory classifier cleanup

* test(channels): cover memory clear expiry

* fix(channels): restore channel memory slash aliases

* test(channels): cover memory intent edge cases
2026-07-07 15:25:01 +00:00
qqqys
467b292b50
feat(channels): add WeCom intelligent robot channel (#6436)
* feat(channels): add WeCom smart bot channel

* fix(channels): harden wecom review suggestions

* fix(channels): address wecom critical review

* fix(channels): include wecom mixed voice text

* fix(channels): tighten wecom outbound media

* fix(channels): harden wecom outbound sends

* fix(channels): address wecom review blockers

* fix(channels): address wecom review followups

* fix(channels): harden wecom inbound handling

* fix(channels): address wecom auth and media review

* fix(channels): tighten wecom inbound cleanup

* fix(channels): harden wecom media safety

* fix(channels): address wecom review typecheck

* fix(channels): harden wecom media review gaps

* fix(channels): address wecom review blockers

* fix(channels): tighten wecom media edge cases

* fix(channels): address wecom review blockers

* fix(channels): address wecom media review blockers

* fix(channels): address wecom review follow-ups

* fix(channels): address wecom review blockers

* fix(channels): close wecom review blockers

* fix(channels): close wecom preflight dedup race

* fix(channels): close wecom review gaps

* fix(channels): harden wecom kick reconnect

* fix(channels): defer wecom session resolution

* fix(channels): clean wecom session attachments

* fix(channels): harden wecom reconnect and media cleanup

* fix(channels): address wecom review diagnostics

* fix(channels): improve wecom diagnostics

* fix(channels): reset wecom kick retries

* fix(channels): improve wecom diagnostics

* fix(channels): preserve sync cancel preflight

* fix(channels): close wecom connection and ssrf gaps

* fix(channels): clean coalesced wecom attachments

* fix(channels): bound wecom sdk connect wait

* fix(channels): scope wecom untracked attachment cleanup

* fix(channels): block wecom nat64 local-use ssrf

* fix(channels): harden wecom media handling

* fix(channels): harden wecom group gates

* fix(channels): bound wecom kick reconnect cycles

* fix(channels): drain loop collect prompts directly

* fix(channels): align wecom buffer hooks

* fix(channels): harden wecom delivery failures

* fix(channels): recover from wecom attachment write failures

* fix(channels): surface wecom media send failures

* fix(channels): harden wecom replay and reconnect

* fix(channels): clarify wecom partial delivery cleanup

* fix(channels): close wecom rejected downloads

* fix(channels): retain wecom dedup after processing starts

* fix(channels): harden wecom reconnect and media errors

* fix(channels): add wecom media error context

* fix(channels): improve wecom dns diagnostics

* fix(channels): keep wecom kick retry alive

* fix(channels): allow wecom quoted bot replies

* fix(channels): preserve wecom code fences across chunks

* fix(channels): harden wecom reconnect lifecycle

* fix(channels): report wecom media dir setup failures

* fix(channels): harden wecom reconnect recovery

* fix(channels): align wecom review fixes

* fix(channels): harden wecom marker parsing

* fix(channels): keep wecom reconnect timers alive

* fix(channels): handle wecom tilde fences

* fix(channels): preserve wecom fence state

* fix(channels): clean up wecom attachment races

* fix(channels): bind wecom media reads to file handles

* fix(channels): prevent wecom symlink media opens

* fix(channels): address wecom review blockers

* fix(wecom): remove media URL from error messages to prevent credential leakage

The guardedHttpsDownload error messages included rawUrl (truncated to 120
chars), which leaks private WeCom media download URLs into stderr and log
aggregation systems. Remove the URL from redirect and HTTP error messages.

* fix(wecom): address review feedback — tests, security, correctness

- Remove stale URL assertions from media download error tests (the error
  messages no longer include raw URLs after the credential-leak fix)
- Redact sensitive fields (secret, aeskey, token, password, authorization)
  in formatSdkError's JSON.stringify fallback to prevent credential
  leakage in logs
- Add indented code block detection to findCodeRanges so [IMAGE: path]
  inside 4-space/tab-indented code is not stripped as a media marker
- Add disconnectGeneration guard before mkdirSync in downloadAttachments
  to prevent orphaned temp directories when disconnect() races with
  in-flight attachment downloads

* fix(wecom): wrap client.disconnect() in catch block to preserve connection error

In the connect() catch block, client.disconnect() could throw (e.g. if
the WebSocket was already destroyed), masking the original connection
error. Wrap in try/catch so cleanup failures never shadow the root cause.

* fix(channels): address wecom reconnect review blockers

* fix(channels): harden wecom reconnect review fixes

* fix(channels): harden wecom review blockers

* fix(channels): address wecom review blockers

* fix(channels): preserve unsupported wecom media markers

* fix(channels): address wecom reliability suggestions

* fix(channels): allow wecom retry after early drops

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-07 15:24:19 +00:00
qqqys
b23f888d73
[codex] add proactive channel loop tools (#6287)
* feat(channel): add proactive loop tools

* fix(channels): stabilize proactive loop routing

* fix(channels): gate loop tools in shared sessions

* fix(channels): tighten channel loop tool routing

* fix(channels): close loop tool review blockers

* fix(dingtalk): preserve markdown tables

* fix(dingtalk): use app token for reactions

* fix(channels): scope loop tools to active caller

* fix(channels): preserve group session metadata

* fix(channels): normalize loop targets

* test(cli): cover settings cron disable path

* fix(channels): address dingtalk review suggestions

* fix(dingtalk): restore table normalization

* fix(channels): mark loop tool failures

* fix(channels): tighten loop mcp protocol handling

* test(channels): cover loop tool guard paths

* fix(channels): await loop mcp registration

* test(channels): preserve base proactive target default

* refactor(channels): clarify loop target promotion

* fix(channels): harden loop recurring input

* fix(channels): ack loop mcp notifications

* fix(channels): preserve legacy loop targets

* test(channels): cover channel loop wiring paths

* fix(channels): retry skipped loop mcp registration

* fix(channels): keep promoted loop targets visible

* fix(channels): harden loop mcp input logging
2026-07-05 15:49:48 +00:00
qqqys
60b9c92b28
Restart stalled ACP bridge for channels (#6330)
* fix(channels): restart stalled ACP bridge

* test(channels): derive acp stall thresholds

* fix(channels): force kill stalled acp bridge

* fix(channels): detect coalesced acp stall logs
2026-07-05 09:36:51 +00:00
曹潇缤
abec702ee2
fix(qqbot): streaming idle-flush with tool-call and stale-callback protection (#6204)
* fix(qqbot): streaming idle-flush with tool-call and stale-callback protection

Add streaming infrastructure to QQ Bot channel:
- streamState Map with per-session buffer and 2s idle-flush timer
- onResponseChunk: accumulates text, resets timer on each chunk
- idleFlush: flushes accumulated buffer, coordinates with pendingStreamDelete
- onToolCall: flushes buffer before tool execution with double-send guard
- onResponseComplete: defers streamState cleanup during flush
- _reconnectId monotonic counter for stale async callback detection
- blockStreaming config-driven guard (skip streaming when enabled)
- All sendMessage calls chained with .catch() for Node 22+ safety

Add 25 stream tests covering:
- idle-flush accumulation and timer reset
- onToolCall immediate flush
- pendingStreamDelete coordination
- Stale reconnect callback guard
- blockStreaming guard
- Concurrent flush prevention

* fix(qqbot): streaming idle-flush with tool-call and stale-callback protection

* fix(qqbot): address review feedback — duplicate-message, chunk-drop, propagate-error, retry-timer, escape-fix

* fix(qqbot): add missing reference guards on streamState.delete

* fix(qqbot): address PR #6204 review feedback

- Extract IDLE_FLUSH_MS and MAX_FLUSH_RETRIES constants
- Add JSDoc to state machine transitions
- Implement onSessionDied lifecycle handler
- Track retryCount per session for bounded retries
- Sanitize log text via sanitizeLogText utility
- Guard zombie timer with !current.timer check
- Clean up flushedSessions and pendingStreamDelete entries
- Add 8 error-recovery test cases (retry, max retries, pendingStreamDelete,
  onToolCall retry, stale closure, disconnect cleanup, onSessionDied,
  flushingSessions guard)
- Fix vitest.config.ts server.deps.inline placement

* refactor(qqbot): extract flushAndTrack helper, add 3 missing guard tests

* fix(qqbot): fix flushedSessions tracking races, add buffer limit and test assertions

* fix(qqbot): remove unused variable to fix ESLint error

* fix(qqbot): restore 4 behaviors missing from PR-C vs feat comparison

- Restore readyTimeout (30s READY guard) in dialGateway()
- Restore heartbeatTimer.unref() in startHeartbeat()
- Restore seenMessages.clear() in disconnect()
- Restore event.author defensive check in handleC2C()

* fix(qqbot): add identity guard to .finally(), rename _reconnectId, remove redundant chatId param

* fix(qqbot): fix recursive flushAndTrack guard bypass and readyTimeout leak in disconnect

* fix(qqbot): address 5 PR-C #6204 review threads - flush guard, stderr newlines, readyTimeout unref, reconnect log

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-04 13:56:49 +00:00
qwen-code-ci-bot
4e3fd29781
chore(release): v0.19.6 (#6280)
* chore(release): v0.19.6

* docs(changelog): sync for v0.19.6

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-03 16:37:54 +00:00
曹潇缤
67da78166b
fix(qqbot): markdown-first send, replyMsgId TTL, and dead code removal (#6201)
* fix(qqbot): markdown-first send with replyMsgId TTL and dead code removal

- Change replyMsgId from Map<string,string> to Map<string,{msgId,timestamp}>
  with 5-minute TTL and periodic cleanup timer
- Add setReplyMsgId() helper with cascaded msgSeqMap cleanup
- Rewrite sendMessage(): markdown-first (msg_type:2), active retry
  on non-4xx failure, plain-text fallback for active messages
- Re-throw errors for .catch() callers instead of silent break
- Update restoreQQState() with backward-compatible replyMsgId migration
- Remove dead code exports: hasMarkdownSyntax, hasLinkSyntax, splitText
- Update send.test.ts: TTL expiry, markdown fallback, noreply suppression,
  replyMsgId helper tests

* fix(qqbot): address review feedback — seq gap, 429 short-circuit, state persistence, input validation

- Fix msg_seq gap on active retry: rollback to nextSeq-1 sends nextSeq, not nextSeq+1
- Add race guard: check replyMsgId still current before updating msgSeqMap on success
- Short-circuit on 429 early: bail after markdown 429 instead of retrying
- Log MESSAGE DROPPED and persist state when both passive+active send fail
- Drain response body on plain-text fallback to prevent socket leak
- Persist after cleanup timer eviction (saveQQState)
- Validate msgSeqMap entries as [string, number] in restoreQQState
- Add Number.isFinite guard for timestamp in restoreQQState
- Eagerly delete expired replyMsgId entries on first TTL check
- Remove redundant saveQQState() calls after setReplyMsgId (handles itself)
- Fix instruction string: remove stale auto-chunk mention (splitText removed)
- Fix misleading log: expired reply says 'without msg_id' not 'active message'

* fix(qqbot): align sendMessage fallback and replyMsgId cleanup with feat branch

* fix(qqbot): address PR #6201 review comments — setReplyMsgId guard, cleanup persistence, TTL constant, test coverage

* fix(qqbot): address PR #6201 review round 2

* fix(qqbot): address PR #6201 review round 3 — add MESSAGE DROPPED prefix to plain-text fallback error log

* fix(qqbot): address PR #6201 review round 4

- Fix catch block: always call saveQQState() regardless of rollbackApplied
- Plain-text fallback log now includes error body text
- Add success logs for active retry and plain-text fallback paths
- Add .unref() to seenCleanupTimer for clean process exit
- Add threat-model comment to group sender-name sanitization tests
- Add saveQQState spy assertions to rollback tests

* fix(qqbot): address PR #6201 review round 5

* fix(qqbot): address PR #6201 review round 6

* fix(qqbot): address PR #6201 review round 8

- Guard far-future timestamps in restoreQQState validation with an upper
  bound (now + REPLY_MSG_ID_TTL_MS) so corrupted state cannot pin entries
  permanently.
- Add test for 429 without msgId returning silently (no fallback/rollback).
- Add test for 429 on plain-text fallback rate-limited path.
- Add test for setReplyMsgId same-msgId guard no-op branch (no delete).
2026-07-03 15:34:52 +00:00
曹潇缤
c1235d8c69
fix(qqbot): security hardening — gateway validation, atomic state, sanitized logging (#6200)
* fix(qqbot): validate gateway URL protocol to prevent SSRF

- Add validateGatewayUrl(): enforce wss:// protocol, warn on unexpected hostnames
- Integrate into fetchGatewayUrl() return path
- Truncate error body in fetchAccessToken() to 80 chars
- Add 6 tests covering protocol rejection, wss acceptance, and edge cases

* fix(qqbot): atomic state persistence and error log sanitization

State persistence hardening:
- Atomic saveQQState() via tmp+renameSync with disposed guard and unref()
- Atomic flushQQState() with {mode: 0o600} permissions
- Entry type validation in restoreQQState() for chatTypeMap and msgSeqMap

Error log sanitization:
- Wrap all user-controlled data in process.stderr.write() with sanitizeLogText()
- Covering: connect retry, sendMessage errors, state persistence failures,
  token refresh, malformed gateway, WebSocket errors, reconnect, and
  C2C/group handler error paths

* fix(qqbot): add replyMsgId validation in restoreQQState()

Add type/length validation for replyMsgId entries when restoring from
persisted state, consistent with the existing chatTypeMap and msgSeqMap
input validation filters. Entries must be strings ≤ 128 chars.

* fix(qqbot): add test coverage for restoreQQState filters, atomic writes, and gateway URL validation

* docs(qqbot): update restoreQQState doc and add disposed guard comment

- Update restoreQQState JSDoc: document validation instead of "trusts persisted JSON"
- Add inline comment explaining why saveQQState has disposed guard but flushQQState doesn't

* docs(qqbot): update validateGatewayUrl docstring to reflect TLS enforcement, not SSRF

* fix(qqbot): address wenshao review — hostname rejection, error sanitization, msgSeqMap validation

* fix(qqbot): address PR review — drain body, narrow gateway hostname

- Drain resp.body?.cancel() in fetchAccessToken error to prevent
  Undici connection leaks on repeated token failures
- Narrow validateGatewayUrl hostname check from broad Tencent
  wildcards (.tencent.com, .tencentcs.com) to only *.qq.com
  to prevent attacker-controlled Tencent Cloud API Gateway
  domains from passing validation

* test(qqbot): tighten token-error assertion to exact match

* test(qqbot): add connect retry sanitization + msgSeqMap edge-case tests

Add test verifying the final connect() retry sanitizes newline/control
characters in the thrown error message. Add tests for fractional,
overflow, and Infinity values in msgSeqMap restore validation to
prevent regression of the Number.isSafeInteger fix.

* fix(qqbot): address wenshao review round 3 — error preservation, validation logging, URL normalization

* test(qqbot): fix connect retry sanitization assertion — sanitizeLogText preserves readable content, not censor words

* fix(qqbot): address wenshao review — error preservation, validation logging, URL userinfo stripping, test coverage

* fix(qqbot): add Array.isArray guards and replyMsgId drop logging in restoreQQState

Prevent TypeError from .filter() on non-array state values (e.g. object
from partial write). Missing Array.isArray() guard caused all three maps
to be lost on a single corrupted section — now each map independently
validates with both truthiness + Array.isArray() before filtering.

Also add replyMsgId drop-count logging (was missing while chatTypeMap
and msgSeqMap already had it).

* fix(qqbot): drain response body in token error path, sanitize URL TypeError, clean tmp on atomic write failure

Three fixes from wenshao review:

1. fetchAccessToken: cancel unconsumed response body to prevent Undici TCP
   socket leak (could exhaust connection pool over hours in long-running daemon)

2. validateGatewayUrl: strip raw URL from TypeError message to prevent
   log injection via malformed URL strings

3. saveQQState/flushQQState: unlinkSync(tmpPath) in catch blocks to
   prevent orphaned .tmp files when renameSync fails (cross-device, Docker)

* fix(qqbot): test Infinity rejection via 1e999 raw JSON, not JSON.stringify nullification

* fix(qqbot): address PR #6200 review — beforeExit hook, key validation, error clarity

- Add beforeExit hook to flush debounced state on abnormal process exit
  (SIGKILL, OOM, crash) when unref'd 500ms timer has pending writes
- Validate map keys (typeof string, ≤256 chars) in restoreQQState filters
  alongside existing value validation to prevent non-string key bloat
- Improve gateway hostname error message to include expected domain

* fix(qqbot): address PR #6200 review round — non-object JSON restore, disposed guard, beforeExit dedup, comment fix

* fix(qqbot): hoist tmpPath declaration before try block in saveQQState

* fix(qqbot): update beforeExit JSDoc to accurately describe behavior

* fix(qqbot): drain response body in fetchGatewayUrl error path
2026-07-03 07:26:13 +00:00
qwen-code-ci-bot
2126474c28
chore(release): v0.19.5 (#6194)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* chore(release): v0.19.5

* docs(changelog): sync for v0.19.5

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-02 13:38:46 +00:00
qqqys
6509e8de08
feat(channels): show lifecycle status in adapters (#6114)
* chore: ignore local worktrees

* docs(channels): design identity and task lifecycle p0

* docs(channels): plan identity and task lifecycle p0

* feat(channels): add identity and task lifecycle metadata

* fix(channels): suppress cancelled tool call lifecycle

* docs: add channel lifecycle status adapter design

* docs: add channel lifecycle status adapter plan

* feat(channels): map telegram lifecycle to typing

* feat(channels): map weixin lifecycle to typing

* fix(channels): reset weixin typing state after failed start

* feat(channels): map dingtalk lifecycle to reactions

* feat(channels): add feishu card status labels

* fix(channels): preserve feishu collapsible status labels

* feat(channels): show feishu lifecycle card status

* fix(channels): cover loop lifecycle metadata

* fix(channels): preserve feishu terminal card status

* chore: remove internal task reports from branch

* fix(channels): clear late lifecycle status starts

* fix(channels): harden lifecycle event edges

* fix(channels): clean adapter lifecycle state

* fix(channels): finalize cancelled lifecycle before cleanup

* fix(feishu): keep cancelled card status visible

* docs(channels): add lifecycle status no-op coverage

* fix(channels): address lifecycle review suggestions

* docs(channels): align lifecycle status documentation

* fix(channels): route adapter stop through lifecycle cancel

* fix(channels): close lifecycle cancellation races

* fix(channels): keep first feishu terminal status

* fix(telegram): guard lifecycle typing updates

* test(qqbot): cover lifecycle status no-ops

* fix(feishu): preserve completed card race status

* docs(lifecycle): align status review docs

* fix(channels): separate pending cancel state

* fix(channels): clean adapter lifecycle status edges

* fix(channels): order clear cancellation lifecycle

* fix(feishu): preserve user stop status label

* fix(channels): suppress loop chunks during pending cancel

* test(channels): use active session in cancel regression

* fix(channels): harden adapter cancellation tests

* fix(channels): sanitize lifecycle tool fields

* fix(channels): route shared tool call lifecycle

* fix(channels): preserve pending cancel intent

* fix(channels): preserve pending cancel intent

* fix(telegram): track typing by session

* fix(feishu): preserve stop status during finalization

* fix(channels): preserve responses after failed cancel

* fix(channels): tighten lifecycle cancellation reasons

* fix(channels): close lifecycle cancel races and validate identity config

Address the outstanding review findings on #6105:

- carry a typed reason on ChannelLoopSkippedError and report disabled
  loops as 'dropped' instead of 'timeout'
- treat a turn as committed once delivery starts: /cancel re-checks
  deliveryStarted after the cancel RPC settles, and neither prompt path
  lets a late-settling cancel rewrite a delivered turn into cancelled
  (or follow a /clear cancellation with completed)
- tag failed lifecycle events with phase: agent vs delivery
- validate identity/memoryScope shape at config parse time instead of
  throwing an opaque TypeError on the first prompt of every session
- guard onPromptStart hooks, pass job.id to loop onPromptStart/End,
  append the boundary block after operator instructions, gate
  /who + /status identity lines on configured identity/memoryScope,
  cache the boundary prompt, and route error logs through
  lifecycleError()

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(qqbot): keep lifecycle mock in sync

* fix(channels): unify cancel state machine and adapter lifecycle edges

Address the remaining review findings on #6114:

- /cancel now delegates to requestActivePromptCancellation — one cancel
  state machine for slash command and adapter stop buttons; the helper
  refuses to claim success once delivery started and sanitizes its logs
- share isTerminalTaskLifecycleType from channel-base instead of four
  hand-rolled terminal checks
- DingTalk: only attach reactions for message ids seen inbound (loop job
  ids no longer trigger doomed emotion API calls), log reaction API
  failures, and recall reactions when a session dies
- Feishu: track userStopped on the card state so every wind-down path
  renders 已停止生成 after a Stop click, and pass terminal labels through
  updateCard's statusLabel param at the remaining baked-text sites
- Weixin: guard the typing .then() against a racing disconnect
- document onTaskLifecycle as the canonical hook (onPromptStart/End are
  back-compat), fix TS4111 bracket access in the DingTalk test

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): keep failed terminal event when cancel settles post-delivery

Self-review follow-up: once delivery started, the catch paths no longer
reconcile a pending cancel — a late-resolving cancel RPC used to flip
cancelled=true there, suppressing the failed emit while the /cancel
handler (seeing deliveryStarted) also declined to emit, leaving a
started task with no terminal lifecycle event at all.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): close self-review findings on adapter lifecycle edges

- DingTalk: track inbound message ids in a capped insertion-ordered set
  instead of the TTL-swept dedup map, so a turn queued minutes behind a
  long predecessor still attaches its reaction
- Feishu: reset userStopped when the cancel RPC fails (later wind-down
  must render the real terminal status), and give handleStop's plain
  message fallback the same ---/label shape the strip regex expects

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): hold streamed chunks while a cancel is pending

Chunks arriving while a /cancel RPC is in flight were pushed straight
into the BlockStreamer, which can send a block on a size/paragraph
threshold before the cancel resolves — leaking output a successful
cancel can never recall. Hold the pending-window chunks instead: replay
them (block streaming + text_chunk transcript) when the cancel fails,
discard them when it succeeds. onResponseChunk stays live through the
window so adapter-accumulated display state has no permanent hole on a
failed cancel; adapters gate visible updates on their own stop flags.

Also stop passing the loop job id to onPromptStart/onPromptEnd (and the
/clear eviction path): the hook contract is inbound platform message
ids, and adapters act on them — cards and reactions keyed to a fake id.
Lifecycle events still carry job.id for correlation.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(feishu): strip every status-label layout from quoted-reply context

Replace the two $-anchored strip regexes in extractCardText with one
line-granular status-block filter. The anchored patterns missed four
layouts the cards actually render: the two-block truncation card
(notice block + label block), terminal labels joined mid-string before
a collapsible panel body, the 停止失败,请重试 label (never in the
alternation), and label-only stopped cards where the divider leads the
text. Tests now assert the real rendered shapes.

Also update the adapter-cancellation suppression test to the new
hold-and-replay chunk semantics from the base layer.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(feishu): ignore loop job ids in prompt hooks

* fix(channels): defer adapter chunks while cancel is pending

* fix(feishu): preserve bare status text in quotes

* fix(channels): release held chunks on failed cancel

* fix(feishu): preserve generated stop fallback status

* fix(telegram): clear typing on dead sessions

* fix(feishu): share status label definitions

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-02 11:01:07 +00:00
jinye
4b372d39ec
feat(channels): add listSessions to ChannelAgentBridge (#6182)
Add optional `listSessions()` method to `ChannelAgentBridge` interface
so channel consumers can enumerate sessions currently attached to the
bridge. Only `DaemonChannelBridge` implements it — reads from the
internal `sessions` Map and `activePrompts` Set to build a snapshot.

The daemon-worker facade forwards the method unconditionally when
present, matching the existing `getAvailableCommands` pattern.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
2026-07-02 10:36:43 +00:00
qqqys
ca61d7827e
feat(channels): add identity and task lifecycle metadata (#6105)
* chore: ignore local worktrees

* docs(channels): design identity and task lifecycle p0

* docs(channels): plan identity and task lifecycle p0

* feat(channels): add identity and task lifecycle metadata

* fix(channels): suppress cancelled tool call lifecycle

* fix(channels): cover loop lifecycle metadata

* fix(channels): harden lifecycle event edges

* fix(channels): finalize cancelled lifecycle before cleanup

* fix(channels): address lifecycle review suggestions

* fix(channels): close lifecycle cancellation races

* fix(channels): separate pending cancel state

* fix(channels): order clear cancellation lifecycle

* fix(channels): suppress loop chunks during pending cancel

* test(channels): use active session in cancel regression

* fix(channels): sanitize lifecycle tool fields

* fix(channels): route shared tool call lifecycle

* fix(channels): preserve pending cancel intent

* fix(channels): preserve responses after failed cancel

* fix(channels): tighten lifecycle cancellation reasons

* fix(channels): close lifecycle cancel races and validate identity config

Address the outstanding review findings on #6105:

- carry a typed reason on ChannelLoopSkippedError and report disabled
  loops as 'dropped' instead of 'timeout'
- treat a turn as committed once delivery starts: /cancel re-checks
  deliveryStarted after the cancel RPC settles, and neither prompt path
  lets a late-settling cancel rewrite a delivered turn into cancelled
  (or follow a /clear cancellation with completed)
- tag failed lifecycle events with phase: agent vs delivery
- validate identity/memoryScope shape at config parse time instead of
  throwing an opaque TypeError on the first prompt of every session
- guard onPromptStart hooks, pass job.id to loop onPromptStart/End,
  append the boundary block after operator instructions, gate
  /who + /status identity lines on configured identity/memoryScope,
  cache the boundary prompt, and route error logs through
  lifecycleError()

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): keep failed terminal event when cancel settles post-delivery

Self-review follow-up: once delivery started, the catch paths no longer
reconcile a pending cancel — a late-resolving cancel RPC used to flip
cancelled=true there, suppressing the failed emit while the /cancel
handler (seeing deliveryStarted) also declined to emit, leaving a
started task with no terminal lifecycle event at all.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): hold streamed chunks while a cancel is pending

Chunks arriving while a /cancel RPC is in flight were pushed straight
into the BlockStreamer, which can send a block on a size/paragraph
threshold before the cancel resolves — leaking output a successful
cancel can never recall. Hold the pending-window chunks instead: replay
them (block streaming + text_chunk transcript) when the cancel fails,
discard them when it succeeds. onResponseChunk stays live through the
window so adapter-accumulated display state has no permanent hole on a
failed cancel; adapters gate visible updates on their own stop flags.

Also stop passing the loop job id to onPromptStart/onPromptEnd (and the
/clear eviction path): the hook contract is inbound platform message
ids, and adapters act on them — cards and reactions keyed to a fake id.
Lifecycle events still carry job.id for correlation.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): defer adapter chunks while cancel is pending

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-02 09:04:07 +00:00
qqqys
727bd47539
feat(channels): add DingTalk proactive send for channel loops (#6174)
DingTalk could only reply through the per-message sessionWebhook, so channel
loops (#6073) rejected /loop add on DingTalk and could never deliver scheduled
results cold. Implement proactive send via the robot groupMessages API with a
self-managed access token (the stream SDK only refreshes on connect, so a
long-lived socket serves a stale token after ~2h), a 15s request timeout so a
hung send cannot freeze the session queue, and errors that carry the DingTalk
error detail so /loop inspect is diagnosable. Group targets only; DM and
webhook-URL fallback targets are rejected up front.

Also give the loop prompt a delivery-contract line so the model treats a
scheduled turn as content to produce rather than an action it must send itself
— without it the model went hunting for a webhook to post through.

Closes #6168

Co-authored-by: Qwen-Coder <noreply@qwen.ai>
2026-07-02 08:58:36 +00:00
jinye
227a60cc93
fix(channels): replace setTimeout(0) drain with turn_complete SSE barrier (#6165)
* fix(channels): replace setTimeout(0) drain with turn_complete SSE barrier

DaemonChannelBridge.prompt() used a setTimeout(0) heuristic to wait for
late SSE text chunks after session.prompt() resolved. This was unreliable
because it depended on microtask timing rather than a deterministic signal.

Replace with a turn barrier that awaits the turn_complete SSE event from
handleEvent, which guarantees all preceding session_update events (text
chunks) have been processed. Also handle turn_error by logging the error
data and resolving the barrier.

Add resolveTurnBarrier calls in both dropSession and cancelSession to
prevent deadlocks when sessions terminate while prompt() is awaiting the
barrier.

* fix(channels): resolve turn barrier before session.cancel() in cancelSession

Move resolveTurnBarrier and abortActivePrompts before the await
session.cancel() call, consistent with dropSession ordering. This
prevents the barrier from hanging if cancel() blocks on a network
timeout.

* fix(channels): race turn barrier against setTimeout fallback for non-SSE paths

DaemonSessionClient.prompt() can return directly without turn_complete
in non-SSE paths (blocking HTTP when subscriptionActive is false,
non-202 responses). The barrier would hang in these cases.

Use Promise.race so turn_complete wins deterministically on the SSE
path (microtask beats macrotask) while setTimeout(0) provides a safe
fallback for non-SSE paths.

Also update the turn_error test to match real SDK behavior where
session.prompt() rejects on turn_error via _dispatchTurnEvent.
2026-07-02 08:48:27 +00:00
jinye
487fb45510
feat(cli): Harden daemon-managed channel worker (#6098)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): harden daemon-managed channel worker

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(cli): address channel worker review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): harden channel worker log supervision

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): redact channel worker snapshot errors

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* perf(cli): precompute channel worker log redactors

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): escalate permanent channel worker failures

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): bound oversized worker log tail discard

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6098)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-01 13:26:38 +00:00
qwen-code-ci-bot
f3ea17bf43
chore(release): v0.19.4 (#6132)
* chore(release): v0.19.4

* docs(changelog): sync for v0.19.4

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-01 13:13:47 +00:00
qqqys
6a5ad453c8
[codex] Add explicit channel memory for messaging channels (#6051)
* chore: ignore local worktrees

* feat(core): add channel memory store

* fix(core): preserve dots in channel memory paths

* fix(core): guard dot channel memory paths

* feat(channels): add channel memory commands

* fix(channels): defer channel memory session invalidation

* feat(channels): inject channel memory into sessions

* fix(channels): serialize channel memory context injection

* fix(channels): harden channel memory context races

* fix(channels): let queued turn retry memory context

* feat(cli): wire channel memory into channel startup

* docs(channels): document channel memory

* fix(channels): harden channel memory handling

* fix(channels): gate channel memory injection

* fix(channels): serialize channel memory clears

* fix(channels): tolerate channel memory read failures

* fix(channels): harden channel memory review gaps

* fix(channels): harden channel memory review gaps

* fix(channels): protect shared channel memory

* test(channels): allow group memory view test

* fix(channels): block group memory writes

* fix(channels): include memory in loop prompts

* fix(channels): preserve session context order after merge

* fix(channels): retry loop memory context after read failure

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-01 09:59:48 +00:00
qqqys
891c59fbaa
feat(channels): add group history backfill (#6074)
* feat(channels): add group history backfill

* fix(channels): harden group history backfill

* fix(channels): address group history review gaps

* fix(channels): apply wildcard group history limit
2026-07-01 16:24:09 +08:00
qqqys
c958e6f6f9
feat(channel): add channel loop support (#6073)
* feat(channel): add proactive routines

* fix(channel): harden scheduled routines

* fix(channel): report Feishu proactive send failures

* fix(channel): reject unsupported threaded routines

* fix(channel): harden routine scheduler lifecycle

* fix(channel): enforce routine quotas atomically

* feat(channel): surface routine lifecycle

* test(channel): align routine job fixtures

* fix(channel): initialize routine lifecycle fields

* fix(channel): harden routine scheduler concurrency

* fix(channel): evict timed out scheduled sessions

* fix(channel): harden routine lifecycle review gaps

* feat(channel): add channel loop support

* test(channel): fix loop scheduler lint

* fix(channel): harden loop lifecycle checks

* test(channel): fix loop scheduler lint

* fix(channel): harden loop review handling

* fix(channel): harden loop timeout handling

* fix(channel): cap loop scheduler concurrency

* fix(channel): harden loop session recovery

* fix(channel): harden loop lifecycle recovery

* fix(channel): harden loop recovery gaps

* fix(telegram): preserve injected bot on first connect
2026-07-01 05:07:21 +00:00
jinye
cf6323bfb5
feat(cli): Add daemon-managed channel worker for serve --channel (#6031)
* feat(cli): add daemon-managed channel worker

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): harden serve channel worker lifecycle

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): cover channel worker edge cases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address channel worker review followups

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): clear channel pidfile after worker exit

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address serve channel review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): harden serve channel worker review issues

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): preserve channel worker exit errors

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): harden daemon channel worker startup

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address channel worker review cleanup

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): track channel worker exit explicitly

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address daemon worker startup review (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address daemon worker disconnect review (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address serve channel review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address channel worker review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-01 01:40:54 +00:00
qqqys
ceb1d307a1
fix(channels): structure DingTalk stream logs (#5998)
* fix(channels): structure DingTalk stream logs

* fix(channels): harden dingtalk downstream handling

* fix(channels): harden dingtalk downstream fields

* fix(channels): address dingtalk review blockers

* fix(channels): validate dingtalk downstream routing

* fix(ci): add serve fast path bundle check
2026-06-30 13:53:04 +00:00
易良
c5fb9fd1e7
fix(ci): create isolated home before tests (#6071)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
2026-06-30 12:42:44 +00:00
jinye
c90e6e7ba4
feat(channels): Add channel agent bridge abstraction (#5978)
* feat(channels): add channel agent bridge abstraction

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): handle bridge session lifecycle cleanup

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): close bridge lifecycle review gaps

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address channel bridge review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-29 10:03:28 +00:00
qqqys
68348e236a
feat(channels): qwen tag — RFC + Phase 0 (multiplayer channel-resident agent) (#5888)
* docs(channels): add "qwen tag" RFC — channel-resident multiplayer agent

Design for a persistent, multiplayer agent that lives in a chat channel
(DingTalk-first), built on the existing channel adapters (qwen channel start,
packages/channels/*) and the qwen serve daemon rather than a new service.
Covers the phased plan: Phase 0 multiplayer identity, Phase 1 proactive engine
(scheduler + cold-group push, daemon migration), Phase 2 channel memory +
governance, plus tradeoffs and resolved design decisions.

Part of #5887

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* feat(channels): qwen tag Phase 0 — multiplayer identity & group-safe commands

In a thread-scoped group every member shares one session, which surfaces a few
gaps in the channel layer. Phase 0 closes them as a backward-compatible
increment on the existing AcpBridge channel path:

- Inject a [sender] marker into the prompt for group turns so the agent can tell
  speakers apart; skipped for 1:1 chats and for already-prefixed re-entries.
- Add Envelope.alreadyPrefixed so collect-mode coalescing does not double-prefix
  the already-tagged buffered text.
- Require "/clear confirm" in groups — a bare /clear no longer wipes the shared
  session for the whole channel; DMs still clear directly.
- Add a read-only /who reporting channel / workspace / session scope without
  creating a session.
- Drop DingTalk group messages with no conversationId, so the shared session is
  never keyed on the expiring sessionWebhook.
- Fix the stale ChannelConfig.dispatchMode JSDoc (runtime default is 'steer').

Adds unit tests for sender attribution, the collect double-prefix guard, the
group /clear confirmation, and the read-only /who.

Part of #5887

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): scope group clear confirmation

* fix(channels): address review on qwen tag Phase 0

- Restrict /clear in a shared (thread) group to config.allowedUsers (when set)
  and require an explicit "confirm"; only the shared case is gated — DMs and
  per-user groups clear directly. Reconcile the RFC (OD-4) with this approach
  (a hyphenated /clear-channel isn't parseable; per-member owner-gate waits on
  the identity model, OD-3/OD-11).
- Sanitize the injected [sender] marker (strip brackets/CR/LF, cap length) so a
  crafted nick can't break out of or spoof the attribution tag.
- Surface to stderr when a collect-mode coalesced re-entry drops buffered turns
  instead of swallowing the error.
- Narrow Envelope.alreadyPrefixed to the literal `true` (internal-only flag).
- Expose DingtalkChannel.isUnroutableGroupMessage and test the
  group-without-conversationId drop; replace flaky setTimeout sequencing in the
  collect test with vi.waitFor; add tests for /who (active + private scope) and
  /clear authorization in a shared group.

Part of #5887

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): preserve group command routing

* fix(channels): harden group routing (map leak, injection, slash attribution)

Address the surviving Phase-0 review criticals:

- /clear now purges every per-session map (sessionQueues, activePrompts,
  collectBuffers), not just instructedSessions, so a long-running gateway
  doesn't leak dead session entries.
- QQ group adapter sanitizes the self-prefixed sender name. It sets
  alreadyPrefixed, which bypasses ChannelBase's [..]/newline/length guard,
  so a crafted QQ nickname could otherwise inject brackets/newlines.
- Unrecognized slash commands in a shared group keep their [sender]
  attribution; recognized commands (a local handler or a forwarded agent
  command) still reach the agent verbatim.
- Sanitize quoted referencedText (strip control chars, cap at 500) so it
  can't inject newlines/instructions or balloon the prompt.
- /who reports only the workspace basename, not the absolute cwd.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): close Phase-0 review criticals (quote injection, /clear cancel, drain log)

- referencedText: strip the wrapper's own delimiters (" [ ]) in addition to
  control chars, so a quoted message can't break out of [Replying to: "..."]
  and inject its own top-level instructions.
- /clear: cancel any in-flight bridge.prompt() for the cleared session(s) and
  drop their buffered follow-ups before purging the maps, so a running turn
  can't deliver a stale response into — or resurrect — the cleared session.
- collect-drain failure log now includes the sessionId and last sender so
  concurrent sessions are diagnosable.
- Extract the duplicated sender-name sanitizer into a shared sanitizeSenderName
  helper used by both ChannelBase and QQChannel; reuse isSharedGroupSession for
  the /who scope note.
- Tests: quote-breakout payload, /clear cancels in-flight, /who in a DM,
  single-scope hasSession/removeSession, and a sanitizeSenderName unit test.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): make group slash-command pass-through race-free and invalidate queued turns on /clear

CRITICAL 1 (sender attribution race): availableCommands is populated
asynchronously by the ACP available_commands_update notification, so a
registration check could see an empty list on a fresh session and wrongly
[sender]-prefix the first real /command — corrupting it into plain text.
Stop inline-prefixing ANY slash-shaped message: pass it through verbatim so
it always parses as a command regardless of load/registration state. Also
widen the command token to include - and : so /compress-fast and /git:commit
parse as commands. Tradeoff: an UNREGISTERED group slash command no longer
carries [sender] attribution (inverse of the earlier R2 ask) — not breaking
real commands is the safer default; flagged for maintainers.

CRITICAL 2 (/clear vs queued followups): a teammate turn that entered
handleInbound() before /clear confirm had already captured the prev.then()
chain and would still run bridge.prompt() against the just-cleared session.
Add a per-session generation counter bumped up-front by /clear; a queued turn
snapshots it at enqueue and bails if it was bumped before the turn dequeued.

Tests: fresh-session/empty-availableCommands pass-through, hyphenated command,
unrecognized command (flagged behavior), non-slash still prefixed; /clear
genuinely awaits a PENDING in-flight turn before confirming; /clear confirm
invalidates an already-queued followup (no resurrection); collect-drain
failure logs the lost count with sessionId + sender to stderr.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): only skip group [sender] tag for real slash commands

The group attribution check keyed off the lenient parseCommand(), which
accepts slash-prefixed paths (e.g. /tmp/foo) as commands, so that prose
reached the model without the [sender] tag. Decide command-vs-prose with
a dedicated isSlashCommand() that mirrors the CLI's classifier
(cli ui/utils/commandUtils.ts isSlashCommand): reject //, /*, a bare /,
and any path separator in the first token. Slash-prefixed paths and
comments now keep their speaker attribution, while real commands
(/compress, /git:commit) still pass through verbatim. Purely lexical, so
it stays race-free against the async command list.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): bound /clear's wait on a wedged in-flight turn

doClear awaited active.done after cancelSession, but active.done only
resolves in the prompt() finally. If the ACP child is wedged (stuck tool
call, not reading stdin, or crashed without closing), cancelSession may
throw (swallowed) or succeed yet the prompt never returns, so active.done
never resolves and /clear — and the whole channel — hangs forever.

Race the wait against a CLEAR_CANCEL_TIMEOUT_MS (3s) timeout and purge
anyway when it wins: the per-session maps are still purged and the
generation already bumped, so a turn that settles later is invalidated.
Cancellation stays best-effort.

Tests: a wedged session whose active.done never resolves still completes
/clear (purges every map, replies "Session cleared") within the timeout
via fake timers — no real wait(ms); plus /clear Confirm / CONFIRM
(mixed-case) is accepted, guarding the handler's .toLowerCase().

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): test QQ group slash branch, log dropped queued turns, reclaim cleared generations

Address review feedback on the Phase-0 qwen-tag /clear + group-slash work:

- QQChannel: cover the previously-untested `isSlash` branch of handleGroup
  — a group `/clear` is forwarded verbatim (no `[sender]` tag) and must NOT
  set `alreadyPrefixed`. Catches a regression that always sets it.
- ChannelBase: when a followup turn is dequeued after `/clear` bumped the
  session generation, log the drop (with sessionId) instead of returning
  silently, so an unanswered queued message is diagnosable.
- ChannelBase: reclaim the bumped `sessionGenerations` entry once the cleared
  session's queue drains (or immediately when nothing was queued), so a
  long-running gateway no longer leaks one entry per `/clear`. Reclamation is
  deferred and guarded so it can't delete an entry a still-queued turn needs,
  and a wedged in-flight turn can't block `/clear`.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): harden qwen-tag group session gating, cancel bounds, and prompt sanitization

Address the latest PR #5888 review batch (4 Criticals + suggestions):

- Treat sessionScope 'single' as a SHARED group session: the shared-scope
  predicate now covers both 'thread' and 'single' (via SHARED_SCOPES), so a
  'single'-scoped group can no longer bypass /clear confirm + allowlist gating
  and wipe the channel-wide __single__ session with a bare /clear.
- Bound the steer-mode wind-down wait: race active.done against
  CLEAR_CANCEL_TIMEOUT_MS (and fire-and-forget the cancel) so a wedged ACP child
  can't pin the session queue forever, matching the /clear hardening.
- Make /clear's cancelSession request fire-and-forget so a wedged cancel request
  can't hang /clear before the bounded active.done wait even starts.
- Strip Unicode line/paragraph separators (U+2028/U+2029) and bidi overrides
  (U+202A-U+202E, U+2066-U+2069) from sender names and quoted text via shared
  sanitize helpers (sanitizeSenderName, new sanitizeQuotedText); route
  referencedText and attachment filenames through the shared quoted sanitizer.
- Add sender/conversation context to the generation-bail drop log and the
  DingTalk unroutable-group-message drop log.
- Tests: fix TS4111 bracket-access in ChannelBase tests (test-inclusive
  typecheck clean); add load-bearing coverage for single-scope sharing, wedged
  steer, wedged /clear cancel, Unicode/bidi sanitization, and the
  generation-reclamation guard fire paths.
- Correct the isSlashCommand JSDoc to document the intentional bare-'/'
  divergence from the CLI classifier; note SessionRouter's by-sender scan does
  not match single-scoped keys (latent today).

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): stop block-streaming on /clear, not just cancel it

doClear flipped active.cancelled = true but never called
active.stopStreaming(), unlike the /cancel handler. In block-streaming
channels, text already buffered in the BlockStreamer could still be
emitted by the idle timer during/after /clear, leaking a stale response
into the just-cleared session. Mirror /cancel: also tear down the
streamer on the cancelled prompt in the clear path.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): gate /clear for single-scope DMs and unstick wedged steer queue

Two follow-up fixes in ChannelBase, both on my recent fixes.

CRITICAL 1: `single` scope maps EVERY sender — group OR DM — to the one
`__single__` session, but the shared-session guard also required `isGroup`,
so anyone who could DM the bot could bare-/clear the channel-wide session
without the confirm + allowedUsers gate. The shared-session predicate now
treats `single` as shared regardless of isGroup (`thread` stays group-only);
/help and /who follow the same predicate.

CRITICAL 2: the bounded steer wait stopped `await active.done` from hanging,
but the replacement turn was still chained behind the wedged turn's
never-resolving sessionQueues tail, so the follow-up (and every later
message) hung forever. On the steer timeout we now re-seed the chain
(prev = resolved) so the next turn starts a fresh chain; the wedged turn
stays cancelled, so a late settle still can't deliver a stale response.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): sanitize attachment filePath and tighten isSlashCommand

The attachment filePath was embedded raw in the prompt while the filename beside it was sanitized. Adapters build the path by basename()-ing the user-supplied filename, so its last segment carries the same attacker-controlled chars (brackets, newlines, U+2028, bidi overrides), letting a crafted filename inject prompt lines via the path. Route the rendered path through the same sanitizeQuotedText neutralization; att.filePath itself is left intact, and benign names render unchanged so the agent's read-file tool still resolves them.

Also tighten isSlashCommand to require parseCommand()'s token charset ([a-zA-Z0-9_:-]+ plus an optional @botname). A non-command-shaped input like /cafe or a zero-width-laden token previously skipped the group [sender] tag yet was not a runnable command, reaching the shared session as unattributed prose. It now keeps its attribution.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): guard activePrompt cleanup against replacement-turn clobber

The per-turn `finally` in ChannelBase deleted `activePrompts[sessionId]`
unconditionally. With the steer-mode bounded wait, this corrupts steer
protection: when turn A wedges (its `bridge.prompt()` never resolves), the
`CLEAR_CANCEL_TIMEOUT_MS` race times out (`steerWedged`), and turn B starts a
fresh chain and re-seeds `activePrompts` with its own entry. When A's prompt
finally settles and reaches its `finally`, the unconditional delete removes
B's entry — so a later turn C sees `activePrompts.get(sessionId) === undefined`
and silently loses steer protection (it forwards verbatim instead of
cancelling + re-prompting).

Fix: capture this turn's own `ActivePrompt` and compare-and-delete — only clear
the entry if it is still ours (`activePrompts.get(sessionId) === promptState`).

Sibling-map audit (same later-settling-wedged-turn clobber):
- collectBuffers: also gated on the same `stillCurrent` flag. A replaced wedged
  turn must not drain the buffer the live replacement turn now owns (reachable
  via mixed-mode single-scope, where a steer turn and collect follow-ups share
  one session). Behavior-preserving: in all serialized flows the turn is still
  current at its finally, so the drain runs exactly as before.
- sessionQueues / sessionGenerations / instructedSessions: not mutated in the
  per-turn finally (only in /clear), so no per-turn clobber. Queued turns are
  already guarded against /clear by the sessionGenerations counter.
- promptState.resolve(), bridge.off, streamer.stop, onPromptEnd: per-turn-owned
  cleanup, kept unconditional (a replaced wedged turn must still release any
  steer/clear waiter racing its done promise).

Adds a deterministic test (manual deferred, no timers/sleeps) reproducing A
wedged -> B replaces -> A settles late, asserting B's entry survives and a
following turn C still engages steer protection. Reverting the guard fails it.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): guard onPromptEnd on superseded turns and strip control chars from sender names

When a steer-replacement turn B re-seeds a session after the bounded
wind-down wait times out, a wedged predecessor turn A can settle late and
run its finally. onPromptEnd hides a session/chat-scoped working indicator
(e.g. the Telegram/WeChat typing indicator keyed by chatId), so A's
previously-unconditional onPromptEnd would stop B's indicator while B is
still working. Guard onPromptEnd with the same stillCurrent identity check
that already protects the per-session map cleanup, so a superseded turn can
no longer clear the successor's indicator. A's own teardown (textChunk
listener, streamer, promptState.resolve) stays unconditional.

Bring sanitizeSenderName to parity with its sibling sanitizeQuotedText by
stripping C0/DEL control chars: a crafted display name with \x07/\x1b
otherwise reaches the [name] prompt tag. Mirror the same strip in the qqbot
send.test.ts mock so a control-char regression in the real helper is caught.

Add a diagnostic stderr line when a steer abandons a wedged turn, matching
the existing queue-drop log.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): preserve path chars in attachment filePath; trim sender names

Addresses ci-bot review on the channel prompt-sanitization paths.

filePath over-sanitization (the main regression): a prior hardening pass
routed the rendered attachment path through sanitizeQuotedText, which strips
the `[`, `]`, and `"` characters. Those are valid, common filesystem path
characters (e.g. a Next.js `app/[slug]/page.tsx` route, a quoted segment, or a
space in a folder name), so stripping them advertised a `saved to:` path that
does not exist on disk and broke the agent's read-file tool. A path rendered
alone on its own line cannot use brackets/quotes/spaces to break out of that
line, so the only real injection risk is characters that break or reorder the
line. Add a path-safe neutralizer, sanitizePromptPath, that strips ONLY C0/DEL
controls (incl. CR/LF), the Unicode line/paragraph separators, and bidi
overrides (the PROMPT_UNSAFE_INVISIBLES set), preserving everything else
byte-intact and without capping length. The human-readable fileName label keeps
sanitizeQuotedText (bracket-stripping is fine for a quoted label).

steer generation-capture ordering: in steer mode the session generation was
snapshotted AFTER the bounded wind-down wait. A concurrent /clear (e.g. another
sender's clear-confirm on a shared session) that bumps the generation DURING
that wait was therefore invisible: the snapshot read the post-clear value, the
dequeue equality guard still held, and the turn ran against the just-cleared
session. Capture preSteerGeneration BEFORE the wait and use it for the guard so
a /clear during the wait is detected and the turn bails.

sender-name fallback: sanitizeSenderName now trims after the length cap and
returns an 'unknown' default, so a name made entirely of strippable chars
(brackets/newlines) no longer renders an anonymous bracket tag. Both call sites
embed the result as a bracket tag with no fallback of their own, so the default
lives in the shared helper; the qqbot test mock mirrors the new contract.

Adds load-bearing, mutation-checked tests for each change (deterministic,
fake-timer driven for the steer/clear race; no real-time waits).

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): stop streamer on steer-cancel and align slash-command trim

Two ci-bot review findings on the steer/slash-command paths in ChannelBase.

1. steer-cancel must stop the BlockStreamer, not just cancel. The steer
   handler flipped active.cancelled but never called stopStreaming(), unlike
   the sibling doClear path. cancelled alone only suppresses NEW chunks: text
   already buffered in a wedged turn's streamer is still flushed by the idle
   timer (~1500ms), which fires before the 3000ms steer wind-down bound, so it
   leaks into the chat after the replacement turn has begun. Mirror doClear and
   stop the streamer immediately after cancelling.

2. parseCommand now trims its input so it agrees with isSlashCommand (which
   already trims). Before, " /help" (leading space, common from IME/copy-paste)
   made isSlashCommand return true — suppressing the group [sender] tag — while
   parseCommand returned null, so the command reached the agent unattributed.
   Trimming closes that attribution gap; it changes nothing else (the
   no-whitespace path is unaffected, and /tmp/foo still has no handler).

Also document the forward-looking late-cancel concern at the steer cancel site:
cancelSession is keyed only by sessionId and the replacement prompt reuses it.
The shipping AcpBridge sends cancel+prompt over one in-order stdin stream with
cancel enqueued first, so the child always processes the cancel before the new
prompt — the replacement turn is safe today. A future network/daemon bridge
with cancel latency could reorder them; the proper fix is turn-scoped
cancellation (a Bridge-contract change), deferred to avoid an API break here.

Tests: a steered wedged turn with buffered streamer text invokes stopStreaming
so the idle-timer flush can't deliver stale text after the replacement turn
begins; " /help" in a group is handled as a command (no [sender] tag, no
forward) while /help and /git:commit still parse. Both are mutation-checked.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): block shell commands in shared sessions; dedupe cancel path

Phase 0 ships shared/group sessions (sessionScope 'thread' and 'single')
but has no per-sender trust model — the [sender] marker is explicitly not
a trust boundary. The pre-existing bang (`!`) handler in ChannelBase runs
arbitrary host shell commands, so in a shared session ANY participant
could run `!rm -rf /`. Gate it with the same isSharedSession predicate
used for the destructive-/clear confirm gate: refuse with a one-line
notice in shared sessions, keep direct execution in a 1:1 session (the
lone user is the operator). isSharedSession is promoted from a closure to
a private method so both gates share one source of truth.

Extract the duplicated cancel + bounded-wait sequence from doClear and the
steer branch into a private cancelAndAwaitActive() helper (the duplication
previously caused steer to miss stopStreaming()). steer keeps its pre-wait
generation snapshot and uses the boolean result; doClear ignores it.

Also: trim the over-verbose steer/clear comment blocks to the load-bearing
why; cap sanitizePromptPath at 1024 chars (defense-in-depth) like its
siblings; and make the qqbot send.test mock use the real sanitizeSenderName
via vi.importActual instead of an inline re-implementation that can drift.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): run onPromptEnd after /clear; only skip it for superseded turns

The per-turn `finally` guarded `onPromptEnd` behind `stillCurrent` alone. When
`/clear` cancels an in-flight turn with NO replacement it deletes that turn's
`activePrompts` entry, so if the cancelled prompt settles late `stillCurrent` is
false and `onPromptEnd` was skipped — leaking the platform cleanup several
adapters do there (Telegram clears its typing interval, DingTalk recalls the
working reaction, Weixin clears typing, Feishu finalizes card state). The
`stillCurrent`-only guard was only meant to stop a STEER-superseded turn from
clobbering its replacement's indicator, but it wrongly also caught the
no-replacement `/clear` case.

Distinguish the two: add `ActivePrompt.superseded`, set on the OLD turn only when
a steer replacement actually takes over the session slot. The `finally` now runs
`onPromptEnd` UNLESS the turn was superseded; the `activePrompts.delete` and the
collect-drain stay `stillCurrent`-gated (a superseded turn must not drain the
replacement's buffer). Regression + collect-drain tests added with mutation
checks.

Also addresses review nits: extract a shared command-token regex constant so
parseCommand and isSlashCommand can't drift; report `single` scope as "shared
channel-wide" in /who (it is shared across all DMs and groups, not just one
group); append a truncation ellipsis in sanitizeQuotedText so a cut
quote/filename is detectable; and include the message text in the
generation-bail drop log so an ignored message is diagnosable.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): require the slash-command token to immediately follow the slash

isSlashCommand used trimmed.slice(1).trimStart() before taking the first token, so a space after the slash (`/ foo`) still classified as a command. parseCommand's regex requires the token immediately after `/`, so it returned null for the same input. In a shared group session that divergence suppressed the [sender] attribution (isSlashCommand true) while running no command (parseCommand null), letting `/ foo` reach the agent unattributed — the exact failure the file's comment warns about.

Remove the .trimStart() so the token must immediately follow the slash; `/ foo` now splits to an empty first token and is treated as prose, agreeing with parseCommand. Normal commands (/help, /git:commit, /compress-fast, /cmd@bot) are unaffected. Adds an invariant test that isSlashCommand and parseCommand agree on `/ foo` (both false) and /help (both true), plus a behavioral test that `/ foo` keeps its [sender] tag.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): don't clobber a post-/clear turn's indicator; tidy refusal + diagnostics

Addresses wenshao's review on PR #5888:

- Refusal text: `!` shell commands are "disabled in shared sessions" (drop
  "group" — isSharedSession is also true for single-scope DMs, which are not
  groups).
- /clear diagnostic: capture cancelAndAwaitActive's result and log on timeout,
  mirroring the steer "abandoned a wedged turn" message, so a wedged /clear is
  observable instead of silently "worked".
- /clear indicator clobber: a /clear that evicts a wedged turn never set
  `superseded`, so the turn's late-settling finally still ran onPromptEnd and
  killed the indicator a turn started AFTER the /clear now owns. Add a
  `clearEvicted` flag: /clear runs the wedged turn's own onPromptEnd at eviction
  time (no replacement exists yet) and marks it clearEvicted; the late finally
  then skips onPromptEnd. onPromptEnd now fires exactly once for the evicted
  turn, at clear-time. The finally guard becomes
  `stillCurrent || (!superseded && !clearEvicted)`. ActivePrompt carries the
  originating chatId/messageId so the eviction can target the right indicator.
- Dropped-queued-turn log: sanitize the attacker-controlled message text
  (render newline visibly, strip C0/DEL incl. CR/ESC) before it reaches an
  operator's terminal, matching every other embed path.

Tests: complete the single-scope DM `!` refusal test (not-called + length +
not-forwarded); update the group/DM/1:1 refusal assertions to the new text;
re-point the "/clear settles late" test to clear-time cleanup; add a
replacement-after-/clear test asserting a late settle does not end a later
turn's indicator; strengthen the dropped-turn-log test with control-char input.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): fix root tsc errors in channel tests; gate /who; sanitize dingtalk nick log

Root `tsc --noEmit` failed on 9 type errors in two channel test files
that per-package builds and `--workspace` typechecks miss (both exclude
test files). Fix the test types only — no production type changes:
- qqbot/dingtalk: `QQChannel`/`DingtalkChannel` come from `await import()`
  as values, so annotate instances with `InstanceType<typeof X>`.
- dingtalk test config was missing the required `token` field of
  `ChannelConfig`.
- qqbot mock: bracket-access the index-signature `router` and type the
  assignment so `{}` is assignable to `Record<string, unknown>`.

/who: gate it to authorized senders in shared sessions, mirroring /clear
— /who leaks the workspace basename, so non-members shouldn't see it.

dingtalk: sanitize `senderNick` with the shared `sanitizeSenderName`
before writing it to stderr, so a crafted nick with CR/LF/control chars
can't fragment or inject log lines.

!-shell gate: refuse `!` shell commands in ALL groups (gate on
`envelope.isGroup`, not just shared sessions) — a user-scope group is not
a shared session yet is still multi-operator, so members could reach the
host shell. The refusal now runs BEFORE router.resolve, so a refused
command never creates a session. Single-scope DMs stay refused.

tests: the qqbot/dingtalk suites mock `@qwen-code/channel-base` and pull
the real `sanitizeSenderName` via the package export, which resolves to
base/dist and broke clean package-local runs. Alias the specifier to
base's source in each vitest config (mirrors cli) so the suites run
without a prior `tsc --build` of base.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): bump generation and release indicator on steer-abandon, mirroring /clear

When a steer abandons a wedged turn it re-seeds a fresh queue chain but, unlike doClear, did neither of the two protections doClear runs on a wedged-turn eviction. Both bugs surface when mixed dispatch modes collapse onto one session (single/user scope).

FIX 1 (generation bump): a followup queued behind the wedged turn stays on the now-orphaned chain. Without bumping sessionGenerations, when the wedged turn late-settles the followup PASSES the dequeue guard and runs the unguarded activePrompts.set, clobbering the live steer replacement -> two concurrent bridge.prompts on one session (duplicated responses + double tool execution). Bump the generation up-front (skipping if a /clear raced the wind-down wait) and advance the replacement's captured snapshot so it proceeds while the orphaned followup bails.

FIX 2 (messageId-scoped indicator leak): the supersede path never released the abandoned turn's OWN indicator. A CHAT-scoped indicator is re-seeded by the replacement, but a MESSAGEID-scoped one (per-message reaction/card keyed on the inbound messageId) is keyed on the abandoned turn's messageId and leaks until disconnect. Run the abandoned turn's onPromptEnd at steer-time and mark it superseded so its late finally still skips onPromptEnd (released once, no double-fire, replacement untouched).

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): steer waits for the running turn instead of racing a concurrent replacement

In steer dispatch, when a new message arrived while a prompt was running,
ChannelBase cancelled the running turn (best-effort), did a BOUNDED wait, and
on TIMEOUT (the old turn wedged, not finished) PROCEEDED to start a replacement
bridge.prompt() on the SAME sessionId while the old prompt was still active. As
wenshao flagged, this is bridge-unsafe:

- DaemonChannelBridge.prompt() REJECTS while the prior prompt is still marked
  active, so the replacement is silently dropped.
- Both bridges collect/emit chunks keyed by sessionId ONLY, so the abandoned
  turn's late chunks mix into the replacement's stream (duplicated/stale output).

Fix (wenshao option (a)): steer now best-effort cancels the running turn and
CHAINS the new turn onto the existing session queue tail, so it runs only AFTER
the old turn's finally has actually run (onChunk detached, activePrompts cleared,
indicator released). The cancel stays — it makes the old turn wind down sooner —
but steer never proceeds while a turn is still active, eliminating the unsafe
concurrency entirely. Steer now differs from followup only by that best-effort
cancel.

Removed the steer-only concurrency machinery that only existed to host a
concurrent replacement: the steer-path bounded wait (cancelAndAwaitActive call),
the steerWedged flag and fresh-chain re-seed, the preSteerGeneration capture, the
steer-time sessionGenerations bump, the steer-time onPromptEnd, and the superseded
flag (plus its finally guard term). /clear's OWN protections — its up-front
generation bump, eviction-time onPromptEnd, and the clearEvicted finally guard —
are SEPARATE and preserved; /clear genuinely evicts and still needs them.
cancelAndAwaitActive is retained, now used only by /clear.

BEHAVIORAL CHANGE: steer no longer force-interrupts a genuinely wedged turn; it
cancels it and waits for it to finish before the new turn runs. Turn-scoped
cancellation/routing (so a new turn can run without waiting for a wedged
predecessor) is wenshao option (b) — it needs an API change across every adapter
and is the deferred enhancement, out of scope here.

Folded in two further #5888 review items:

- [Critical] QQ slash-command audit-log injection (qqbot/src/QQChannel.ts): the
  audit process.stderr.write interpolated the RAW, attacker-controlled senderName
  (event.author.username) and cleanText BEFORE sanitization, so a crafted nick or
  message with CR/LF/ANSI escapes could forge or corrupt operator audit logs.
  Hoisted `safeName = sanitizeSenderName(senderName)` above the audit log and now
  log a neutralized command string (cap 80, render \n visibly, strip C0/DEL),
  mirroring ChannelBase's dropped-turn log and the DingTalk hardening already in
  this PR. The prompt-path sanitizeSenderName usage is unchanged.

- [Suggestion] single-scope DMs were not attributed (base/src/ChannelBase.ts): the
  [sender] prefix was gated on envelope.isGroup alone, but sessionScope:'single'
  collapses every sender's DM into one __single__ session (already treated as
  shared by the !-gate, /clear confirm and /who), so different people merged into
  one unattributed conversation (the RFC-R4 gap Phase 0 closes). The gate is now
  (envelope.isGroup || sessionScope === 'single') — deliberately NOT
  isSharedSession, which is false for user-scope groups that must keep attribution.

Tests: removed the obsolete steer-concurrency cases and added cases for the new
steer behavior (new turn starts only after the old completes; the abandoned turn's
late chunks cannot reach the new turn). Added an audit-log sanitization test
(QQ) and single-scope-DM / user-scope-group / 1:1-DM attribution tests (base).
/clear's eviction protections are unchanged and still pass.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): neutralize NEL/C1 in prompt text; dedupe shared-session auth gate

Address wenshao's review on the Phase-0 qwen-tag channel base.

- sanitize: PROMPT_UNSAFE_INVISIBLES now also neutralizes the C1 control
  block (U+0080-U+009F), which includes NEL (U+0085), a Unicode line break
  (UAX#14 BK) that renders as a new line. Without it a display name like
  "Alice<NEL>system: ..." or a crafted reply quote could inject a prompt
  line. Applies to both sanitizeSenderName and sanitizeQuotedText (and the
  shared path sanitizer) since they share the set. Tests cover NEL + a second
  C1 (CSI U+009B) for sender names and quoted text; mutation-checked.

- ChannelBase: the two best-effort cancelSession() calls (the /clear
  wind-down wait and the steer pre-cancel) no longer swallow the IPC failure
  with an empty catch. They now log channel name + sessionId + reason to
  stderr, matching the existing /cancel log style, so a wedged turn is
  diagnosable. Still fire-and-forget (not awaited).

- ChannelBase: extract the verbatim shared-session authorization gate shared
  by /clear and /who into a private isAuthorizedForSharedSession() predicate
  (isSharedSession + allowedUsers check). Behavior is unchanged; each caller
  keeps its own rejection wording.

- tests: add a GROUP-path case that /help@mybot is treated as a command
  (no [sender] prefix, not forwarded to the agent); the existing @botname
  test only covered a DM.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): strip C1/NEL from audit-log text too, matching the prompt path

The prompt path now neutralizes the C1 control block (U+0080-U+009F, incl.
NEL U+0085, a Unicode line break) via PROMPT_UNSAFE_INVISIBLES, but the inline
stderr audit-log sanitizers still stripped only C0/DEL. A crafted message
carrying NEL/C1 therefore survived into the dropped-/queued-turn log
(ChannelBase) and the QQ slash-command audit log, where many terminals render
NEL as a newline, forging an extra [channel] log line.

Extend both inline strips to also cover the C1 block (C0 + DEL + C1), matching
the prompt path's union, and refresh the now-stale comments. Tests feed a NEL
(U+0085) and a C1 char (U+009B) through both log paths and assert neither
survives; reverting the strip to C0/DEL fails the new NEL assertions.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): gate /status and /cancel in shared sessions; code-point-safe truncation

Three follow-ups to the shared-session authorization work:

- /status: add the same isAuthorizedForSharedSession gate /who uses, so a
  non-member of a shared session with a non-empty allowedUsers list can no
  longer read its session/access state. Non-shared (DM/per-user) is unchanged.
- /cancel: gate the destructive abort behind isAuthorizedForSharedSession like
  /clear (auth gate only, no confirm), so a group member can't cancel another
  user's in-flight turn on a shared session. 1:1 and authorized users unchanged.
- sanitize: truncate sanitizeSenderName/sanitizeQuotedText/sanitizePromptPath on
  Unicode code-point boundaries (Array.from) instead of UTF-16 code units, so a
  cap landing mid-surrogate-pair (e.g. an emoji) can't leave a lone surrogate
  that renders as the replacement character downstream. The ellipsis logic in
  sanitizeQuotedText still keeps the result within maxLen code points.

Adds tests for each (incl. mutation-checked gates and an emoji-at-cap case).

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): audit-log shared /clear and blocked ! shell; test steer stopStreaming

Address ci-bot review on PR #5888:

- [Observability] Emit a stderr audit line on a successful clear of a
  SHARED session (channel, sessionId, sanitized sender + stable senderId).
  A 1:1 DM clear is single-participant and is not logged.
- [Observability] Emit a stderr audit line when a group/shared member's
  `!` host-shell command is refused, so operators can detect blocked
  attempts. Sender name is sanitized; the command payload is not echoed.
- [Test gap] Add a steer test asserting the best-effort cancel calls
  active.stopStreaming (spying on the running turn's prompt), plus tests
  for both new audit lines and the DM no-log case.

The /cancel and /status shared-session auth gates flagged by ci-bot were
already present on this branch and are left unchanged.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): gate steer-cancel by authorization; close audit-log + attribution injection gaps

Address review on PR #5888 (qwen-tag-phase0):

- [Critical] The /cancel auth gate was bypassable via the default `steer`
  dispatch mode: any normal message cancels the running turn, so an
  unauthorized member of a shared session - blocked from /cancel - could
  abort another user's active turn just by sending a message. The steer
  branch now checks isAuthorizedForSharedSession FIRST and, when
  unauthorized, breaks to normal queuing (the message chains onto the
  session queue tail and runs AFTER the active turn instead of aborting it).

- [Suggestion] Audit-log sanitizers missed U+2028/U+2029 and the bidi
  overrides (log-line spoofing / trojan-source). Exported
  PROMPT_UNSAFE_INVISIBLES and added a shared sanitizeLogText(text, maxLen)
  helper that applies BOTH that set AND the C0/DEL strip (and caps length),
  used at both audit-log sites (ChannelBase dropped-turn log + QQ
  slash-command audit log) so the defense can't drift apart.

- [Suggestion] The [sender] attribution prefix was suppressed for any
  command-SHAPED text, so unrecognized "/x\n[SYSTEM]: ..." reached the agent
  unattributed. Added a synchronous isRecognizedCommand() (locally registered
  commands + the bridge.availableCommands snapshot) and now suppress the
  prefix only when the text is BOTH a command shape AND a recognized command;
  unrecognized command-like text keeps its [sender] tag.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): recognize command aliases per-session so attribution doesn't break them

isRecognizedCommand decides whether to suppress the group [sender]
attribution prefix, but matched only against availableCommands[].name.
The ACP parser also accepts command ALIASES (parseSlashCommand altNames),
so a valid alias like /summarize (alias of /compress) or /login (alias of
/auth) was classified as unrecognized, rewritten to "[Alice] /summarize",
and then run as PLAIN CHAT (the leading / is gone) instead of executing.
It also read the bridge's GLOBAL availableCommands snapshot, which in
DaemonChannelBridge can belong to another session.

- Carry aliases through the available-command contract: add an optional
  altNames?: string[] to AvailableCommand, emit them on the wire in
  _meta.altNames (ACP's extension point; a top-level altNames would be an
  excess-property error against the SDK type), and lift them in both
  bridges via readAvailableCommandAltNames. Omitted when absent, so
  alias-free entries stay byte-identical.
- Match name AND altNames, against THIS session's command list:
  isRecognizedCommand now takes sessionId and reads
  getAvailableCommands(sessionId) when the bridge exposes it
  (DaemonChannelBridge), falling back to the global getter (AcpBridge,
  single-agent, inherently session-correct). Stays synchronous: a real
  command sent before the snapshot loads keeps its tag (safe default).
- Security intent intact: genuinely-unrecognized command-shaped text
  (e.g. /x\n[SYSTEM]: ...) still keeps its [sender] tag.

Also fold in two #5888 review items in ChannelBase.ts:
- Steer auth gate: audit the silent steer->queue downgrade to stderr for
  an unauthorized member (operator-visible only; no per-message reply),
  matching the /cancel,/clear,/who,/status gates' observability.
- /clear eviction: a clear-time onPromptEnd that throws would abort the
  purge, leaving the evicted turn in activePrompts so its late finally
  (stillCurrent || !clearEvicted) re-runs onPromptEnd and clobbers a newer
  turn. Set clearEvicted first and catch+audit the throw so the purge
  always runs (turn becomes non-current) and the late finally skips.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): match agent commands case-sensitively; add steer wedge watchdog

isRecognizedCommand lowercased the token before comparing it to agent
command names/aliases, but the CLI's parseSlashCommand matches agent
commands CASE-SENSITIVELY (`cmd.name === part`, `cmd.altNames?.includes(part)`).
So a wrong-case token like `/SUMMARIZE` was "recognized" here — suppressing
the `[sender]` attribution tag — yet ran NO command in ACP, which then
forwarded the raw text unattributed, reopening the injection where a crafted
`/SUMMARIZE\n[SYSTEM]: …` second line reaches a shared group as an apparent
system directive. parseCommand now also returns the typed-case `raw` token;
isRecognizedCommand matches AGENT commands on `raw` (case-sensitive, mirroring
the CLI) while LOCAL commands keep their existing case-INSENSITIVE match
(registerCommand lowercases the stored name; handleInbound dispatches by the
lowercased token).

The steer chain-and-wait path lost the old "abandoned wedged turn" log, so a
hung predecessor bridge.prompt() could silently deadlock the session with no
observability. Add a diagnostic-only watchdog: the steer branch arms a timer
(unref'd) that, if the predecessor is still the active prompt after
CLEAR_CANCEL_TIMEOUT_MS, emits a stderr line pointing at /clear for recovery;
the chained `.then()` disarms it as its first statement once the predecessor's
tail resolves. No concurrency change — chain-and-wait is untouched.

Also simplify the late-finally onPromptEnd guard from
`stillCurrent || !clearEvicted` to `!clearEvicted`: clearEvicted is set ONLY
by /clear's eviction, which then unconditionally deletes activePrompts (its
try/catch around the clear-time onPromptEnd guarantees the purge even if it
throws) and never re-inserts the same promptState, so `clearEvicted` implies
`!stillCurrent` and the dropped term was unreachable.

Tests: wrong-case `/SUMMARIZE` keeps `[sender]` (with a mutation note); wrong-
case local `/HELP` still dispatches locally; fake-timer watchdog tests for the
wedged (logs) and settled (timer cleared, no log) paths.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): align agent command recognition with parseSlashCommand; guard finally onPromptEnd

Address maintainer review on the qwen-tag Phase 0 channel adapter.

- [Critical] Guard the normal-completion onPromptEnd in the per-turn finally.
  onPromptEnd runs platform-adapter cleanup (typing interval, working-reaction
  recall, card finalize) - network/IO that can throw. An uncaught throw skipped
  activePrompts.delete (session leak), promptState.resolve (active.done never
  settled, so a later /clear falsely logged "abandoned a wedged turn"), and the
  collect-buffer drain - and the rejection, swallowed by the queue tail's
  .catch(() => {}), silently dropped every later turn. Wrap it in try/catch with a
  stderr log, matching the eviction-path treatment.

- [Critical] End the channel/agent command-recognition divergence by matching the
  AGENT branch of isRecognizedCommand EXACTLY as the CLI's parseSlashCommand does:
  the FIRST whitespace token after the leading '/', case-SENSITIVELY, WITHOUT
  stripping an @suffix. parseCommand's '@'-stripped, lowercased token diverged from
  the agent (PARSE_COMMAND_RE drops '(?:@\S+)?'), so /compress@x or /Compress were
  "recognized" here (tag suppressed) yet ran no command there and reached the model
  unattributed. Matching the exact token leaves wrong-case / @suffix / injection-
  shaped tokens UNRECOGNIZED -> they keep their [sender] tag (attributed), exactly as
  the agent treats them. No prompt rewrite: a /compress@otherbot aimed at another bot
  must not run here. LOCAL commands keep case-insensitive dispatch.

- [Suggestion] Include the originating chatId/messageId (sanitized) in the "/clear
  abandoned a wedged turn" log so oncall can correlate the stuck turn.

- [Suggestion] Clarify the alreadyPrefixed JSDoc: it is also set by the QQ adapter on
  real self-prefixed inbounds, which sanitize the embedded name at the source - so the
  flag does not bypass sanitization (verified; no behavior change needed).

- [Suggestion] Replace the blind `as unknown as {...}` bridge cast on the recognition
  path with a typed AgentCommandsProvider interface (optional members), so a future
  rename/return-type change is type-checked instead of breaking at runtime.

- [Suggestion] Validate altNames' shape: isAvailableCommand now rejects a non-array
  altNames, and the recognition site guards the alias check with Array.isArray, so a
  malformed wire payload can't throw at the `.includes` call.

Tests: throwing-onPromptEnd cleanup + collect-drain + log; exact-token recognition
(verbatim /compress and /summarize alias; tag kept for /SUMMARIZE, /COMPRESS,
/compress@x, and /compress@x + [SYSTEM] line); wedged-turn log carries chat/message;
malformed altNames does not throw; isAvailableCommand drops a malformed-altNames entry.
Restored stderr spies in the two steer-watchdog tests' finally.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(channels): address qwen tag review followups

---------

Co-authored-by: Qwen-Coder <noreply@qwen.ai>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-06-29 07:17:13 +00:00
qwen-code-ci-bot
8b65a555a0
chore(release): v0.19.3 (#5952)
* chore(release): v0.19.3

* docs(changelog): sync for v0.19.3

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-28 06:59:17 +00:00
tt-a1i
714513df20
feat(channels): register Telegram bot command menu (#5919) 2026-06-27 22:53:08 +00:00
qwen-code-ci-bot
5bb79bc67c
chore(release): v0.19.2 (#5830)
* chore(release): v0.19.2

* docs(changelog): sync for v0.19.2

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-24 15:00:58 +00:00
jinye
a234860a4a
fix(core): Align MCP OAuth guidance and docs (#5589)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* docs: Align docs with current CLI behavior

Update stale documentation and user-facing MCP OAuth guidance to match the current dialog-based flows, SDK permission semantics, current links, and Qwen OAuth status.

Also replace Ink internal imports with public Ink APIs for the shared text input so the workspace builds against Ink 7.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: fix BaseTextInput Ink import (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): surface MCP OAuth credential read failures

Fix SSE OAuth credential pre-check failures by reporting token storage read errors before connecting.

Update SDK coreTools docs and extension release link text from the follow-up review.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): harden MCP OAuth error handling

Handle stderr warning failures as best-effort and keep SSE 401 OAuth guidance when credential re-read fails.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): keep SSE OAuth pre-read best effort

Avoid blocking SSE MCP connections when the diagnostic credential pre-read fails, and cover BaseTextInput absolute-position edge cases.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): handle SSE OAuth validation errors

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): surface MCP OAuth recovery guidance

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): cover MCP OAuth retry paths

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): address OAuth guidance review

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-24 07:09:53 +08:00
qwen-code-ci-bot
8eb5770812
chore(release): v0.19.1 [skip ci]
* chore(release): v0.19.1

* docs(changelog): sync for v0.19.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-23 17:26:10 +08:00
qwen-code-ci-bot
57156522bd
chore(release): v0.19.0 [skip ci]
* chore(release): v0.19.0

* docs(changelog): sync for v0.19.0

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-23 11:38:15 +08:00
qwen-code-ci-bot
8f8ed0d7c1
chore(release): v0.18.5 [skip ci]
* chore(release): v0.18.5

* docs(changelog): sync for v0.18.5

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 21:13:19 +08:00
qwen-code-ci-bot
c5fb75b5c2
chore(release): v0.18.4 [skip ci]
* chore(release): v0.18.4

* docs(changelog): sync for v0.18.4

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 03:36:38 +00:00
tt-a1i
295860db24
fix(telegram): clear typing intervals on disconnect (#5477) 2026-06-21 01:52:57 +08:00
tt-a1i
fc15ae6cb7
fix(dingtalk): skip uppercase webhook reaction targets (#5466) 2026-06-21 01:51:48 +08:00
tt-a1i
5924ab3a95
fix(weixin): handle uppercase CDN upload schemes (#5439) 2026-06-20 16:16:28 +08:00
tt-a1i
409a612fec
fix(channel): bound qqbot gateway reconnect retries (#5415) 2026-06-20 06:33:34 +08:00
tt-a1i
617eb20081
fix(channel): keep qqbot token refresh retrying (#5414) 2026-06-20 02:32:32 +08:00
tt-a1i
30bfe63528
fix(channel): track qqbot close reconnect timer (#5416) 2026-06-20 02:18:24 +08:00
tt-a1i
8477560b8d
fix(channel): scope qqbot session backup path (#5417) 2026-06-20 02:13:43 +08:00
曹潇缤
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
Yufeng He
5a84ba016b
fix(weixin): confirm the WEBP signature, not just the RIFF prefix (#5285)
detectImageMime treated any data starting with the four RIFF bytes as
image/webp, but RIFF is a generic container also used by WAV and AVI. A
non-WebP RIFF file (e.g. a WAV renamed to .webp) therefore passed the
magic-byte check in validateImagePath, which exists to confirm a file's real
type. Verify the 'WEBP' marker at bytes 8-11 as well, matching the stricter
check already in core's imageTokenizer and the earlier full-signature PNG fix.
2026-06-19 04:23:38 +08:00
tt-a1i
a45b14fba3
fix(weixin): normalize markdown image syntax (#5297) 2026-06-19 04:07:23 +08:00
tt-a1i
06fdd59cf2
fix(dingtalk): split oversized markdown lines (#5299) 2026-06-19 03:10:42 +08:00
tt-a1i
0a0fedf984
fix(weixin): show allowed image directories (#5296) 2026-06-19 02:47:32 +08:00
qwen-code-ci-bot
45220c219c
chore(release): v0.18.3 [skip ci]
* chore(release): v0.18.3

* docs(changelog): sync for v0.18.3

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-18 00:28:48 +08:00