Skip to content

fix(followup): prevent tool call UI leak and Enter accept buffer race#2872

Merged
yiliang114 merged 14 commits intoQwenLM:mainfrom
wenshao:fix/followup-hide-from-toolcall-ui
Apr 8, 2026
Merged

fix(followup): prevent tool call UI leak and Enter accept buffer race#2872
yiliang114 merged 14 commits intoQwenLM:mainfrom
wenshao:fix/followup-hide-from-toolcall-ui

Conversation

@wenshao
Copy link
Copy Markdown
Collaborator

@wenshao wenshao commented Apr 4, 2026

Summary

Follow-up suggestion generation API calls were leaking into the tool call UI. Additionally, accepting a suggestion via Enter left the suggestion text stuck in the input buffer due to a microtask race condition. This PR fixes both issues, plus improves suggestion quality and hardens internal prompt filtering.

Root Cause 1 — Tool call UI leak

Leak path Symptom
Forked query included tools in generation config Model could return function calls instead of plain text
logApiResponse/logApiError wrote to chatRecordingService Suggestion API events appeared in session JSONL and WebUI as tool calls
logToolCall wrote to chatRecordingService unconditionally Internal tool calls leaked to chat recordings
LoggingContentGenerator recorded requests/responses Suggestion I/O appeared in OpenAI logs

Root Cause 2 — Buffer race on Enter accept

When accepting a followup suggestion via Enter, accept() queued buffer.insert(suggestion) in a microtask, which executed after handleSubmitAndClear had already cleared the buffer — leaving the suggestion text stuck in the input. Fixed by adding a skipOnAccept option to accept() for the Enter path, which bypasses the onAccept callback since the text is passed directly to submit.

Changes

New utils/internalPromptIds.ts — Centralises internal prompt IDs (prompt_suggestion, forked_query, speculation) in a ReadonlySet with an exported isInternalPromptId() guard. Adding a new internal ID requires changing only this file.

followup/forkedQuery.tsrunForkedQuery sets tools: [] via a deep-frozen NO_TOOLS constant (Object.freeze with as const) in the per-request config, preventing the model from producing function calls. createForkedChat retains the full generationConfig (including tools) so speculation callers can still execute tool calls.

telemetry/loggers.tslogApiResponse, logApiError, and logToolCall skip chatRecordingService.recordUiTelemetryEvent() for internal prompt IDs. uiTelemetryService.addEvent() is preserved so /stats still tracks suggestion token usage. Removed unused isInternalPromptId re-export (all consumers import directly from utils/internalPromptIds.js).

loggingContentGenerator.ts — For internal prompt IDs:

  • Skips logApiRequest, buildOpenAIRequestForLogging, and logOpenAIInteraction
  • loggingStreamWrapper skips response collection and consolidation to reduce CPU/memory overhead
  • Tracks firstResponseId/firstModelVersion during iteration so _logApiResponse/_logApiError retain accurate IDs even without full response collection

followup/followupState.tsaccept() gains an optional { skipOnAccept: true } parameter. When set, the microtask skips the onAccept callback (buffer insert) while still firing telemetry and clearing state.

followup/suggestionGenerator.ts — Improved SUGGESTION_PROMPT:

  • FIRST: directive now prioritizes the last few lines of the assistant's response (where tips and next-step hints appear)
  • PRIORITY: section extracts actionable text from explicit tips (e.g., Tip: type post commentspost comments)
  • Two new examples for tip extraction

InputPrompt.tsx (CLI) / InputForm.tsx (WebUI) — Enter accept path passes { skipOnAccept: true } to prevent the microtask from re-inserting the suggestion after the buffer was already cleared by submit.

Documentation — Updated prompt-suggestion-design.md (LLM Prompt section synced, new Internal Prompt ID Filtering section) and followup-suggestions.md (tip extraction mention in user docs).

Not affected

  • /stats token tracking ✅
  • Speculation (speculative execution) ✅ — now also filtered from chat recordings
  • Normal conversation logging and recording ✅
  • Tab / Right Arrow accept paths ✅ (no skipOnAccept, behavior unchanged)

Test plan

  • 172 related tests pass (followupState, forkedQuery, suggestionGenerator, internalPromptIds, loggers, loggingContentGenerator, speculation, overlayFs, speculationToolGate, smoke)
  • skipOnAccept unit test: skips onAccept callback, still fires telemetry
  • Enter accept test: verifies buffer.insert is NOT called
  • logToolCall internal ID filtering: 3 prompt IDs verified
  • loggingContentGenerator internal ID filtering: speculation included
  • internalPromptIds test: all 3 IDs + negative cases
  • CLI: suggestion ghost text appears in input prompt after task completion
  • CLI: Tab/Enter/Right Arrow correctly accept suggestions
  • CLI: Enter accept clears the input buffer (no leftover text)
  • /stats includes suggestion token counts
  • WebUI session replay no longer shows suggestion API calls as tool calls
  • Speculation feature works (tools available in speculative loop)

… tool call UI

The follow-up suggestion generation was leaking into the conversation UI
through three channels:

1. The forked query included tools in its generation config, allowing the
   model to produce function calls during suggestion generation. Fixed by
   setting `tools: []` in runForkedQuery's per-request config (kept in
   createForkedChat for speculation which needs tools).

2. logApiResponse and logApiError recorded suggestion API events to the
   chatRecordingService, causing them to appear in session JSONL files
   and the WebUI. Fixed by adding isInternalPromptId() guard that skips
   chatRecordingService for 'prompt_suggestion' and 'forked_query' IDs.
   uiTelemetryService.addEvent() is preserved so /stats still tracks
   suggestion token usage.

3. LoggingContentGenerator logged suggestion requests/responses to the
   OpenAI logger and telemetry pipeline. Fixed by skipping logApiRequest,
   buildOpenAIRequestForLogging, and logOpenAIInteraction for internal
   prompt IDs. _logApiResponse is preserved (for /stats) but its
   chatRecordingService path is filtered by fix #2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

📋 Review Summary

This PR addresses a clean separation between internal background operations (suggestion generation, forked queries) and user-visible session logging. The changes prevent internal API calls from appearing in tool call UI, session JSONL recordings, and WebUI replays while preserving token usage tracking for /stats. The implementation is well-structured with consistent patterns across three files.

🔍 General Feedback

  • Consistent pattern: The isInternalPromptId() helper is implemented in both LoggingContentGenerator and loggers.ts, providing a clear guard mechanism for internal operations
  • Minimal changes: Each modification is surgical and focused on the specific goal of filtering internal operations from persistent logs
  • Good preservation: Token usage tracking via _logApiResponse is explicitly preserved for /stats functionality
  • Clear documentation: JSDoc comments explain the purpose of the internal prompt ID guard functions

🎯 Specific Feedback

🟡 High

  • File: packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts:82 and packages/core/src/telemetry/loggers.ts:130 - Duplicate isInternalPromptId() implementation: The same function is implemented in two separate files. This creates maintenance burden and risk of divergence. Consider extracting this to a shared utility module (e.g., packages/core/src/utils/internalPromptIds.ts) or a common constants file that both modules can import.

    Suggested fix: Create a shared utility:

    // packages/core/src/utils/internalPromptIds.ts
    export function isInternalPromptId(promptId: string): boolean {
      return promptId === 'prompt_suggestion' || promptId === 'forked_query';
    }

🟢 Medium

  • File: packages/core/src/followup/forkedQuery.ts:195 - Magic string for tools: []: The comment explains the intent well, but the magic empty array could be extracted to a named constant for clarity and to prevent accidental modification. Consider using Object.freeze([]) or a named constant like NO_TOOLS_CONFIG.

    Suggested improvement:

    const NO_TOOLS_CONFIG: GenerateContentConfig = Object.freeze({ tools: [] });
    const requestConfig: GenerateContentConfig = { ...NO_TOOLS_CONFIG };
  • File: packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts:155-165 - Inconsistent null handling for openaiRequest: When isInternal is true, openaiRequest is set to undefined, but the type system may not reflect this properly. The logOpenAIInteraction calls are guarded, which is correct, but consider making the type explicit or using a more explicit pattern.

🔵 Low

  • File: packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts:78-83 - JSDoc could mention specific internal operations: The comment mentions "suggestion generation, forked queries" which is good, but could also reference the specific prompt ID values for easier cross-referencing.

  • File: packages/core/src/telemetry/loggers.ts:125-130 - Consider exporting isInternalPromptId for testing: If this function needs to be tested in isolation, it should be exported. Currently it's private to the module which may make unit testing the guard logic more difficult.

✅ Highlights

  • Excellent test plan: The PR description includes a comprehensive test plan covering all affected functionality (ghost text, suggestion submission, /stats, WebUI replay, speculation feature)
  • Clear separation of concerns: The distinction between runForkedQuery (no tools) and createForkedChat (retains tools for speculation) is well-implemented and documented
  • Preserves important functionality: Careful attention to preserving _logApiResponse for token tracking while filtering UI-visible logging shows good architectural awareness
  • Minimal invasive changes: The guards are placed at the right abstraction level, avoiding the need to thread parameters through multiple layers

Copy link
Copy Markdown
Collaborator Author

@wenshao wenshao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

整体方案比纯 UI 层修复更彻底,赞同从源头解决。补充几点建议:

1. 提取 isInternalPromptId 消除重复(对应 Bot 高优建议)

isInternalPromptId()loggers.tsloggingContentGenerator.ts 中各有一份实现,逻辑完全一致。建议提取到共享工具模块,例如:

// packages/core/src/utils/internalPromptIds.ts
export const INTERNAL_PROMPT_IDS = Object.freeze({
  SUGGESTION: "prompt_suggestion",
  FORKED_QUERY: "forked_query",
}) as { readonly [key: string]: string };

export function isInternalPromptId(promptId: string): boolean {
  return promptId in INTERNAL_PROMPT_IDS;
}

使用 Object.freeze + 常量映射而非硬编码字符串比较,后续新增内部 promptId 时只需改一处,避免两份实现产生分歧。

2. tools: [] 建议提取为常量

forkedQuery.ts 中的 { tools: [] } 建议提取为命名常量:

const NO_TOOLS_CONFIG: GenerateContentConfig = Object.freeze({ tools: [] });
// ...
const requestConfig: GenerateContentConfig = { ...NO_TOOLS_CONFIG };

3. openaiRequest = undefined 的类型安全

内部 promptId 时 openaiRequest 被赋值为 undefined,但 logOpenAIInteraction 的参数类型可能不接受 undefined。建议将 openaiRequest 声明为 SomeType | undefined,或在调用处增加类型守卫,确保 TypeScript 无 warning。

4. 补充 isInternalPromptId 的单元测试

建议验证 "prompt_suggestion" → true、"forked_query" → true、"user_query" → false、空字符串 → false。

— qwen3.6-plus

…ers.ts

Address review feedback: extract isInternalPromptId() to a single
exported function in telemetry/loggers.ts and import it in
LoggingContentGenerator, eliminating the duplicate private method.

Also update loggingContentGenerator.test.ts mock to use importOriginal
so the real isInternalPromptId is available during tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wenshao wenshao requested a review from Copilot April 4, 2026 05:37
@wenshao
Copy link
Copy Markdown
Collaborator Author

wenshao commented Apr 4, 2026

已处理 review 反馈,提交 3c28d03fa

🟡 High — 重复 isInternalPromptId() 实现
已修复。将 isInternalPromptId() 提取为 telemetry/loggers.ts 的 export 函数,LoggingContentGenerator 直接 import 使用,消除重复。同时更新测试 mock 使用 importOriginal 以暴露真实的 isInternalPromptId

🟢 Medium — tools: [] magic value
保持现状。tools: []requestConfig 初始化处有清晰注释说明意图,提取为 Object.freeze 常量反而增加间接性,且此处是唯一使用点。

🟢 Medium — openaiRequest null 处理
logOpenAIInteraction 参数类型已为 OpenAI.Chat.ChatCompletionCreateParams | undefined,且第 315 行有 if (!openaiRequest) return 的 early-return guard。当前行为正确。

🔵 Low — JSDoc 和 export
JSDoc 已补充具体 prompt ID 值(prompt_suggestion, forked_query)。函数已改为 export 可用于测试。

Address maintainer review feedback:

1. Move isInternalPromptId() to packages/core/src/utils/internalPromptIds.ts
   using a ReadonlySet for the ID registry. Adding new internal prompt IDs
   only requires changing one file. loggers.ts re-exports for compatibility,
   loggingContentGenerator.ts imports directly from utils.

2. Extract `tools: []` magic value to a frozen NO_TOOLS constant in
   forkedQuery.ts.

3. Add unit tests for isInternalPromptId: prompt_suggestion → true,
   forked_query → true, user_query → false, empty string → false.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wenshao
Copy link
Copy Markdown
Collaborator Author

wenshao commented Apr 4, 2026

已处理维护者反馈,提交 662a123b4

1. 提取 isInternalPromptId 到共享 utils
已创建 packages/core/src/utils/internalPromptIds.ts,使用 ReadonlySet<string> 管理内部 prompt ID 集合。新增内部 ID 只需改一处。loggers.ts 通过 re-export 保持兼容,loggingContentGenerator.ts 直接从 utils 导入。

2. tools: [] 提取为常量
已在 forkedQuery.ts 中提取为 const NO_TOOLS: GenerateContentConfig = Object.freeze({ tools: [] }),使用处改为 { ...NO_TOOLS }

3. openaiRequest 类型安全
logOpenAIInteraction 参数类型已为 OpenAI.Chat.ChatCompletionCreateParams | undefined,第 315 行有 if (!openaiRequest) return guard,TypeScript 编译无 warning。

4. 单元测试
已添加 internalPromptIds.test.ts,覆盖:prompt_suggestion → true、forked_query → true、user_query → false、空字符串 → false、其他任意 ID → false。150 个测试全部通过。

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prevents internal follow-up/suggestion generation requests from surfacing as tool-call UI events by treating them as “internal” prompt IDs and suppressing their recording/logging in the session replay artifacts.

Changes:

  • Added isInternalPromptId() and used it to skip chatRecordingService recording for api_response / api_error events tied to internal prompt IDs.
  • Updated forked query execution to strip tools per request (tools: []) so follow-up generation can’t produce function calls.
  • Updated LoggingContentGenerator to skip request/OpenAI interaction logging for internal prompt IDs while preserving API response/error metrics logging.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

File Description
packages/core/src/telemetry/loggers.ts Adds internal prompt ID detection and prevents internal API events from being recorded into session JSONL/WebUI replay.
packages/core/src/followup/forkedQuery.ts Forces forked queries to run with no tools to avoid tool-call UI artifacts.
packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts Skips request/OpenAI logging for internal prompt IDs while keeping response/error metric logging.
packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts Adjusts logger mocking to preserve new exports while still mocking request/response/error logging.
Comments suppressed due to low confidence (3)

packages/core/src/followup/forkedQuery.ts:211

  • runForkedQuery now explicitly overrides tools to an empty list to prevent function calls. There’s test coverage for CacheSafeParams in this module, but nothing asserting that runForkedQuery actually sends requests with tools stripped (or that it cannot emit tool-call parts). Add a unit test that stubs GeminiChat.sendMessageStream/content generator and verifies the merged config contains tools: [] for the 'forked_query' prompt_id.
  const model = options?.model ?? params.model;
  const chat = createForkedChat(config, params);

  // Build per-request config overrides.
  // NO_TOOLS prevents the model from producing function calls — forked
  // queries are pure text completion and must not appear in tool-call UI.
  const requestConfig: GenerateContentConfig = { ...NO_TOOLS };
  if (options?.abortSignal) {
    requestConfig.abortSignal = options.abortSignal;
  }
  if (options?.jsonSchema) {
    requestConfig.responseMimeType = 'application/json';
    requestConfig.responseJsonSchema = options.jsonSchema;
  }

  const stream = await chat.sendMessageStream(
    model,
    {

packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts:217

  • For internal prompt IDs, openaiRequest is set to undefined (so OpenAI logging is effectively disabled), but the streaming path still runs the full response consolidation work in loggingStreamWrapper before calling logOpenAIInteraction (which then returns early). To avoid unnecessary CPU/memory overhead for suggestion/forked queries, propagate isInternal into the wrapper and skip consolidating + logOpenAIInteraction entirely when internal.
    const isInternal = isInternalPromptId(userPromptId);
    if (!isInternal) {
      this.logApiRequest(
        this.toContents(req.contents),
        req.model,
        userPromptId,
      );
    }
    const openaiRequest = isInternal
      ? undefined
      : await this.buildOpenAIRequestForLogging(req);

    let stream: AsyncGenerator<GenerateContentResponse>;
    try {
      stream = await this.wrapped.generateContentStream(req, userPromptId);
    } catch (error) {
      const durationMs = Date.now() - startTime;
      this._logApiError('', durationMs, error, req.model, userPromptId);
      if (!isInternal) {
        await this.logOpenAIInteraction(openaiRequest, undefined, error);
      }
      throw error;
    }

    return this.loggingStreamWrapper(
      stream,
      startTime,
      userPromptId,
      req.model,
      openaiRequest,
    );

packages/core/src/telemetry/loggers.ts:133

  • isInternalPromptId and the new guards in logApiError/logApiResponse change recording behavior (skip chatRecordingService.recordUiTelemetryEvent for internal prompt IDs). There are existing logger unit tests, but none exercise this new branch. Add tests that pass prompt_id='prompt_suggestion'/'forked_query' with a mocked getChatRecordingService() and assert recordUiTelemetryEvent is not called, while it still is for normal prompt IDs.

// Re-export for consumers that import from this module.
export { isInternalPromptId } from '../utils/internalPromptIds.js';

export function logStartSession(
  config: Config,
  event: StartSessionEvent,
): void {
  QwenLogger.getInstance(config)?.logStartSessionEvent(event);
  if (!isTelemetrySdkInitialized()) return;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

1. Update forkedQuery.ts module docs to reflect that runForkedQuery
   overrides tools: [] at the per-request level while createForkedChat
   retains the full generationConfig for speculation callers.

2. Propagate isInternal into loggingStreamWrapper to skip response
   collection and consolidation for internal prompts, avoiding
   unnecessary CPU/memory overhead.

3. Add logApiResponse chatRecordingService filter tests: verify
   prompt_suggestion/forked_query skip recording while normal IDs
   still record.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Address Copilot review round 3:

1. Deep-freeze NO_TOOLS.tools array to prevent shared mutable state
   across forked query calls.

2. Add LoggingContentGenerator tests verifying that internal prompt IDs
   (prompt_suggestion, forked_query) skip logApiRequest and OpenAI
   interaction logging while preserving logApiResponse.

3. Add logApiError chatRecordingService filter tests matching the
   existing logApiResponse coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Clarify that createForkedChat retains the full generationConfig
(including tools) for speculation callers, while runForkedQuery
strips tools at the per-request level via NO_TOOLS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

packages/core/src/followup/forkedQuery.ts:224

  • requestConfig is now always non-empty because it always includes tools: [] via NO_TOOLS, so Object.keys(requestConfig).length > 0 ? requestConfig : undefined is redundant and will always select requestConfig. Consider simplifying to pass config: requestConfig directly (or keep the empty-config branch only if NO_TOOLS can be conditionally applied).
  // Build per-request config overrides.
  // NO_TOOLS prevents the model from producing function calls — forked
  // queries are pure text completion and must not appear in tool-call UI.
  const requestConfig: GenerateContentConfig = { ...NO_TOOLS };
  if (options?.abortSignal) {
    requestConfig.abortSignal = options.abortSignal;
  }
  if (options?.jsonSchema) {
    requestConfig.responseMimeType = 'application/json';
    requestConfig.responseJsonSchema = options.jsonSchema;
  }

  const stream = await chat.sendMessageStream(
    model,
    {
      message: [{ text: userMessage }],
      config: Object.keys(requestConfig).length > 0 ? requestConfig : undefined,
    },

packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts:217

  • The internal-prompt behavior is updated for generateContentStream (skipping logApiRequest, OpenAI request conversion, and OpenAI interaction logging), but the new tests only cover generateContent. Add a focused unit test for generateContentStream with userPromptId in ['prompt_suggestion','forked_query'] to assert: logApiRequest/OpenAILogger.logInteraction are not called, while logApiResponse still is (for /stats).
  async generateContentStream(
    req: GenerateContentParameters,
    userPromptId: string,
  ): Promise<AsyncGenerator<GenerateContentResponse>> {
    const startTime = Date.now();
    const isInternal = isInternalPromptId(userPromptId);
    if (!isInternal) {
      this.logApiRequest(
        this.toContents(req.contents),
        req.model,
        userPromptId,
      );
    }
    const openaiRequest = isInternal
      ? undefined
      : await this.buildOpenAIRequestForLogging(req);

    let stream: AsyncGenerator<GenerateContentResponse>;
    try {
      stream = await this.wrapped.generateContentStream(req, userPromptId);
    } catch (error) {
      const durationMs = Date.now() - startTime;
      this._logApiError('', durationMs, error, req.model, userPromptId);
      if (!isInternal) {
        await this.logOpenAIInteraction(openaiRequest, undefined, error);
      }
      throw error;
    }

    return this.loggingStreamWrapper(
      stream,
      startTime,
      userPromptId,
      req.model,
      openaiRequest,
    );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

1. Fix NO_TOOLS type: Object.freeze produces readonly array incompatible
   with ToolUnion[]. Use Readonly<Pick<>> instead; spread in requestConfig
   already creates a fresh mutable copy per call.

2. Fix test missing required 'model' field in ContentGeneratorConfig.

3. Track firstResponseId/firstModelVersion in loggingStreamWrapper so
   _logApiResponse/_logApiError have accurate values even when full
   response collection is skipped for internal prompts.

4. Strengthen OpenAI logger test assertion: assert OpenAILogger was
   constructed (not guarded by if), then assert logInteraction was
   not called.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

1. Simplify runForkedQuery: requestConfig always has tools:[] from
   NO_TOOLS spread, so the Object.keys().length > 0 ternary is dead
   code. Pass requestConfig directly.

2. Add generateContentStream test for internal prompt IDs to match
   the existing generateContent coverage, ensuring the streaming
   wrapper also skips logApiRequest and OpenAI interaction logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@wenshao wenshao added the DDAR DataWorks Data Agent Ready label Apr 4, 2026
@wenshao wenshao changed the title fix(core): prevent followup suggestion I/O from appearing in tool call UI fix(followup): prevent tool call UI leak and Enter accept buffer race Apr 5, 2026
@wenshao wenshao requested a review from Copilot April 5, 2026 11:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

When accepting a followup suggestion via Enter, accept() queued
buffer.insert(suggestion) in a microtask that executed after
handleSubmitAndClear had already cleared the buffer, leaving the
suggestion text stuck in the input.

Add skipOnAccept option to accept() so the Enter path bypasses the
onAccept callback. Also add runForkedQuery unit tests verifying
tools: [] is passed in per-request config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@yiliang114 yiliang114 self-assigned this Apr 7, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator Author

@wenshao wenshao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Core package changes are well-implemented with good test coverage (183 tests pass). However, two critical issues need attention:

Critical

  1. WebUI createFollowupController does NOT implement skipOnAccept — The interface declares accept(method?, options?) with { skipOnAccept?: boolean }, but the actual implementation silently ignores the second argument. The microtask still fires onAccept, re-inserting the suggestion text after the buffer was cleared. This is the exact same race condition the PR fixes in the CLI, but still present in the WebUI.

  2. 'speculation' missing from INTERNAL_PROMPT_IDS — Only 'prompt_suggestion' and 'forked_query' are included. Speculation uses the same LoggingContentGeneratorloggers.tschatRecordingService pipeline, so speculation tool calls will still leak into chat recordings — the same problem this PR fixes for followup suggestions.

Suggestion

  1. WebUI duplicates createFollowupController — Full ~110-line re-implementation from core. This duplication is the root cause of issue (1). Consider importing from core directly or using a thin adapter.

— qwen3.6-plus via Qwen Code /review

@wenshao
Copy link
Copy Markdown
Collaborator Author

wenshao commented Apr 8, 2026

回复 qwen3.6-plus review:

  1. WebUI skipOnAccept — 已确认 WebUI 侧 createFollowupController 未实现 skipOnAccept。该问题需要独立修复,不在本 PR 范围内(本 PR 聚焦 core 层 internal prompt 过滤)。
  2. 'speculation' 缺失 — ✅ 已修复,'speculation' 已加入 INTERNAL_PROMPT_IDS,logToolCall 也补齐了 isInternalPromptId 过滤。
  3. WebUI 重复 createFollowupController — 属于代码结构优化,建议后续单独 PR 处理。

@wenshao
Copy link
Copy Markdown
Collaborator Author

wenshao commented Apr 8, 2026

[Critical] @qwen-code/webui no longer exports the documented @qwen-code/webui/followup subpath.

On main, this is a published public entrypoint backed by packages/webui/src/followup.ts, but this PR removes both the export and the subpath build artifact. Existing consumers importing @qwen-code/webui/followup will fail to resolve the package after upgrading.

Suggested fix: restore the ./followup export in packages/webui/package.json and keep a compatibility re-export file, even if the root entrypoint now also exports the same symbols.

— gpt-5.4 via Qwen Code /review

…, improve suggestion prompt

- Add 'speculation' to INTERNAL_PROMPT_IDS so speculation API traffic
  and tool calls are hidden from chat recordings and tool call UI
- Add isInternalPromptId check to logToolCall() for consistency with
  logApiError/logApiResponse
- Improve SUGGESTION_PROMPT: prioritize assistant's last few lines and
  extract actionable text from explicit tips (e.g. "Tip: type X")
- Fix garbled unicode in prompt text
- Update design docs and user docs to reflect changes
- Add test coverage for all new behavior
@wenshao
Copy link
Copy Markdown
Collaborator Author

wenshao commented Apr 8, 2026

回复 gpt-5.4 review:

误报。 @qwen-code/webui/followup subpath 的移除不是本 PR 的改动,而是已合入 main 的 PR #2902 (c2ba09635) 做的。该 PR 将 followup controller 内联到 useFollowupSuggestions.ts 并从主入口导出,有意移除了独立 subpath。

本 PR 的 diff 中包含该变更是因为 merge main 时引入的,不需要恢复。

…erator tests

- Object.freeze NO_TOOLS and its tools array to prevent runtime mutation
- Add 'speculation' to loggingContentGenerator internal prompt ID tests
  for consistency with loggers.test.ts and internalPromptIds.ts
wenshao added 2 commits April 8, 2026 10:42
Use `as const` with type assertion to satisfy TypeScript while keeping
runtime immutability via Object.freeze.
…rs.ts

All consumers import directly from utils/internalPromptIds.js.
The re-export was dead code with no importers.
Copy link
Copy Markdown
Collaborator

@yiliang114 yiliang114 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core / CLI fixes look good to me. I’m treating the exported WebUI followup consumer alignment as separate follow-up work, tracked in #3030, rather than a blocker for this PR.

@yiliang114 yiliang114 merged commit f208801 into QwenLM:main Apr 8, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DDAR DataWorks Data Agent Ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants