mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-22 23:25:28 +00:00
* feat(web-shell): support mutable default mid-turn messages * fix(serve): register mid-turn removal telemetry route * test(serve): update telemetry route totals * fix(test): add session_mid_turn_message_mutation to expected features list * fix(webui): forward clientId on cross-session mid-turn removal (#8229) - Forward the session clientId in the cross-session removeMidTurnMessage branch so the bridge's exact-originator match can succeed; without it the removal resolved to an undefined originator and could never remove the message stamped at enqueue. - Strip a misaligned/malformed messageIds from mid_turn_message_injected in asKnownDaemonEvent instead of rejecting the whole event, mirroring the sidechannel parser so a buggy daemon can't silently lose the injection signal. - Log a mid-turn removal miss in the bridge like the enqueue/pending-removal siblings, to make removal races diagnosable from daemon logs. * fix(web-shell): exclude annotations from mid-turn path and harden idle cleanup (#8229) * fix(web-shell): add container-type to .queuedPrompts so @container query applies (#8229) * fix(web-shell): harden mid-turn dedupe and capability gate per review (#8229) - removeInjectedFromQueue now matches by id first (position-independent) and falls back to text only when no id match exists, so two same-text sends can't remove the wrong row and double-deliver. - Thread canMutateMidTurn into useQueuedPrompts and gate the mid-turn delete/edit mutation on it, so the keyboard path can't hit a DELETE route the daemon doesn't advertise. - asMidTurnMessageInjectedData omits a malformed messageIds key instead of leaving a present undefined, matching the sidechannel parser. - Narrow MidTurnQueueItem.midTurnState, document the load-bearing effect order, and make clearQueuedPrompts return false on a no-op clear. * fix: harden mid-turn removal per review (log escape, cross-session client id) (#8229) - Escape the caller-controlled messageId (and sessionId) in the mid-turn removal-miss stderr line to prevent log injection (CWE-117). - Forward the target session's persisted client id on cross-session mid-turn removal so the bridge's exact-originator match no longer rejects valid removals after a session switch with per-session client ids. - Strengthen tests: distinct-id independence for two queued messages, deferred removal proving the composer waits for daemon removal, and the active-turn delete failed-action flag. --------- Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
export interface MidTurnQueueItem {
|
|
text: string;
|
|
images?: unknown[];
|
|
midTurnState?: 'submitting' | 'queued';
|
|
midTurnMessageId?: string;
|
|
}
|
|
|
|
export interface MidTurnInjectedBatch {
|
|
sessionId: string;
|
|
messages: readonly string[];
|
|
messageIds?: readonly string[];
|
|
/** Trusted client id that queued the messages (from the SSE envelope). */
|
|
originatorClientId?: string;
|
|
}
|
|
|
|
/**
|
|
* Reconcile injected mid-turn messages against the local pending queue: remove
|
|
* the entry matching each injected message for `sessionId`, across ALL `batches`
|
|
* (a multi-batch turn drains once per tool batch, so the consumer must process
|
|
* every accumulated batch, not just the latest).
|
|
*
|
|
* Each injected message is matched in two passes. The first pass is a strict
|
|
* `midTurnMessageId` match (the daemon mints an id at admission and echoes it on
|
|
* injection); it wins regardless of array position, so two same-text sends can't
|
|
* steal each other's removal when their admission responses arrive out of order.
|
|
* Only when no id match exists does the second pass fall back to the first
|
|
* text-only entry with matching text — any mid-turn row when the batch carries
|
|
* no ids (older daemon), or a still-`submitting` row that hasn't received its id
|
|
* yet. Matching stays count-based — one removal per injected message — so a
|
|
* queue that holds the same text twice loses one entry per matching injection.
|
|
* Entries carrying images are never matched: image messages aren't pushed
|
|
* mid-turn (the drain channel carries plain strings), so they stay queued for
|
|
* the next turn. An entry that already fell back to the ordinary path
|
|
* (`midTurnState === undefined`) is never matched.
|
|
*
|
|
* Skips a batch whose `originatorClientId` is some OTHER client: the daemon
|
|
* broadcasts the injection frame to every client on the session, but only the
|
|
* client that queued the message should drop it — a peer with a coincidentally
|
|
* equal text must keep its own entry. Batches with no originator (anonymous
|
|
* push) are reconciled regardless.
|
|
*
|
|
* Returns a NEW array when something was removed, or `null` when nothing matched
|
|
* (so the caller can skip a redundant state update).
|
|
*/
|
|
export function removeInjectedFromQueue<T extends MidTurnQueueItem>(
|
|
prompts: readonly T[],
|
|
batches: readonly MidTurnInjectedBatch[],
|
|
sessionId: string,
|
|
clientId?: string,
|
|
): T[] | null {
|
|
const remaining = [...prompts];
|
|
const isTextOnly = (prompt: T) =>
|
|
!prompt.images || prompt.images.length === 0;
|
|
let changed = false;
|
|
for (const batch of batches) {
|
|
if (batch.sessionId !== sessionId) continue;
|
|
if (
|
|
batch.originatorClientId !== undefined &&
|
|
batch.originatorClientId !== clientId
|
|
) {
|
|
continue;
|
|
}
|
|
for (const [messageIndex, message] of batch.messages.entries()) {
|
|
const messageId = batch.messageIds?.[messageIndex];
|
|
// A strict id match wins regardless of position; the text fallback below
|
|
// only runs for rows the id can't reach (no ids in the batch, or a row
|
|
// still awaiting its admission id).
|
|
let index =
|
|
messageId !== undefined
|
|
? remaining.findIndex(
|
|
(prompt) =>
|
|
prompt.midTurnState !== undefined &&
|
|
prompt.midTurnMessageId === messageId &&
|
|
isTextOnly(prompt),
|
|
)
|
|
: -1;
|
|
if (index < 0) {
|
|
index = remaining.findIndex(
|
|
(prompt) =>
|
|
prompt.midTurnState !== undefined &&
|
|
(messageId === undefined ||
|
|
(prompt.midTurnState === 'submitting' &&
|
|
prompt.midTurnMessageId === undefined)) &&
|
|
prompt.text === message &&
|
|
isTextOnly(prompt),
|
|
);
|
|
}
|
|
if (index >= 0) {
|
|
remaining.splice(index, 1);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
return changed ? remaining : null;
|
|
}
|