Skip to content

fix(core): normalize cumulative OpenAI stream deltas to suffixes#3896

Merged
wenshao merged 9 commits into
mainfrom
fix/openai-cumulative-delta-normalize
May 13, 2026
Merged

fix(core): normalize cumulative OpenAI stream deltas to suffixes#3896
wenshao merged 9 commits into
mainfrom
fix/openai-cumulative-delta-normalize

Conversation

@chiga0

@chiga0 chiga0 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Some OpenAI-compatible upstreams (notably specific DashScope / 阿里云百炼 Coding Plan paths) send delta.content as accumulated full text instead of incremental suffixes. The Gemini stream pipeline appends every chunk verbatim, so the same content gets concatenated repeatedly during streaming — visible as Markdown / table content duplicating itself many times before the final chunk lands.

This PR adds a per-stream normalizeStreamingTextDelta that detects cumulative-mode chunks (current chunk starts with previously emitted text) and emits only the suffix. Once cumulative mode is locked in, exact-repeat and prefix-overlap chunks are silenced. Normal incremental streams flow through unchanged.

The fix applies to both delta.content and delta.reasoning_content / delta.reasoning, with separate state slots so a switch between text and reasoning channels does not cause false-positive suppression.

Why split out

Extracted from #3663 (umbrella TUI flicker / streaming stability PR — ~30 files / ~4300 LoC across UI bounding, scrollback artifact suppression, capture infra, etc.). This single-purpose PR isolates the provider-side normalization layer because:

  • It is fully packages/core only — zero ink / react coupling, unaffected by the in-flight ink 7 upgrade.
  • It is independently measurable — see fix(cli): harden TUI flicker and streaming output stability #3663's table-sentinel evidence: with cumulative chunks, sentinel labels go 8 → 112 (amplification 14×). This PR alone drops that ratio back to 1.0×.

The umbrella will be split into a series of small, independently demonstrable PRs; this is the first.

Issues this addresses

Family / closed but related context: #2402 (CLOSED, OpenRouter duplicate finish_reason chunks — different fix, same family of OpenAI-compat upstream quirks).

Note: each of the above issues may also have a UI-side contributor (narrow-window scrollback amplification, etc.) that is not addressed here. Those will land via separate follow-ups from #3663.

Test plan

  • cd packages/core && npx vitest run src/core/openaiContentGenerator/converter.test.ts — 83 / 83 pass, including 3 new cases:
    • should normalize cumulative streaming content deltas to suffixes — happy path
    • should ignore repeated cumulative chunks with no new suffix — exact-repeat lock-in
    • should preserve repeated short incremental content chunks — false-positive guard
  • npm run typecheck --workspace=@qwen-code/qwen-code-core — clean
  • npm run lint --workspace=@qwen-code/qwen-code-core — clean
  • Full core test suite: 268 files / 6991 passed / 3 skipped — no regressions
  • Live reproduction GIF on a known-cumulative provider (before/after) — to be attached

Live reproduction GIF

the following gif shows the duplicate output when the delta.content output the full content.

comparison

Reproduction

On main with a known-cumulative provider: streaming a long Markdown table prints the table contents N+(N-1)+(N-2)+… times during the live frame loop.

On this branch: the same stream prints the contents exactly once, regardless of how many cumulative chunks the upstream emits.

For deterministic synthetic reproduction, the capture harness in #3663 (integration-tests/terminal-capture/streaming-clear-storm.ts, payload markdown-table-cumulative) is the canonical script — left out of this PR to keep the diff minimal but planned as a follow-up split.

Follow-ups

Next planned splits from #3663 (each independently demonstrable, non-conflicting with the ink 7 upgrade):

@chiga0 chiga0 requested review from tanzhenxin and wenshao May 7, 2026 06:51
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI 75.56% 75.56% 76.55% 80.26%
Core 78.24% 78.24% 80.83% 82.6%
CLI Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   75.56 |    80.26 |   76.55 |   75.56 |                   
 src               |   73.64 |    67.35 |   76.47 |   73.64 |                   
  gemini.tsx       |   61.11 |    59.13 |   66.66 |   61.11 | ...18,835-838,846 
  ...ractiveCli.ts |   80.02 |    68.61 |   78.57 |   80.02 | ...1021,1059,1162 
  ...liCommands.ts |   76.17 |    73.33 |     100 |   76.17 | ...50-274,299,401 
  ...ActiveAuth.ts |     100 |     87.5 |     100 |     100 | 66-80             
 ...cp-integration |   53.94 |    66.66 |   58.82 |   53.94 |                   
  acpAgent.ts      |   56.25 |    67.01 |   65.51 |   56.25 | ...71-873,887-895 
  authMethods.ts   |   12.19 |      100 |       0 |   12.19 | 11-31,34-38,41-50 
  errorCodes.ts    |       0 |        0 |       0 |       0 | 1-22              
  ...DirContext.ts |     100 |      100 |     100 |     100 |                   
 ...ration/service |   68.65 |    83.33 |   66.66 |   68.65 |                   
  filesystem.ts    |   68.65 |    83.33 |   66.66 |   68.65 | ...32,77-94,97-98 
 ...ration/session |   76.02 |    70.59 |      84 |   76.02 |                   
  ...ryReplayer.ts |   65.93 |    75.67 |   81.81 |   65.93 | ...40-255,268-269 
  Session.ts       |   75.12 |    68.89 |    85.1 |   75.12 | ...2456,2462-2465 
  ...entTracker.ts |   90.85 |    84.84 |      90 |   90.85 | ...35,199,251-260 
  index.ts         |       0 |        0 |       0 |       0 | 1-40              
  ...ssionUtils.ts |   84.21 |    77.77 |     100 |   84.21 | ...37-153,209-211 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ssion/emitters |   96.01 |    90.75 |    92.3 |   96.01 |                   
  BaseEmitter.ts   |   76.92 |    66.66 |      80 |   76.92 | 23-24,39-40,55-56 
  ...ageEmitter.ts |     100 |    89.47 |     100 |     100 | 109,111           
  PlanEmitter.ts   |     100 |      100 |     100 |     100 |                   
  ...allEmitter.ts |   98.06 |     92.3 |     100 |   98.06 | 227-228,327,335   
  index.ts         |       0 |        0 |       0 |       0 | 1-10              
 ...ession/rewrite |   90.36 |    87.83 |   94.11 |   90.36 |                   
  LlmRewriter.ts   |      81 |       84 |     100 |      81 | ...,88-89,155-159 
  ...Middleware.ts |   95.83 |    85.71 |     100 |   95.83 | 119,127-129       
  TurnBuffer.ts    |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/auth          |   97.68 |    94.85 |   95.45 |   97.68 |                   
  allProviders.ts  |     100 |      100 |     100 |     100 |                   
  ...iderConfig.ts |    97.6 |    95.04 |     100 |    97.6 | ...61,411,433-434 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/auth/install  |   98.57 |    88.88 |     100 |   98.57 |                   
  ...nstallPlan.ts |   98.57 |    88.88 |     100 |   98.57 | 80,93             
 ...viders/alibaba |   96.96 |    66.66 |   66.66 |   96.96 |                   
  ...baStandard.ts |     100 |      100 |     100 |     100 |                   
  codingPlan.ts    |   93.67 |    66.66 |   66.66 |   93.67 | 83,87-89,94       
  tokenPlan.ts     |     100 |      100 |     100 |     100 |                   
 ...oviders/custom |     100 |      100 |     100 |     100 |                   
  ...omProvider.ts |     100 |      100 |     100 |     100 |                   
 ...roviders/oauth |    91.5 |    77.03 |   97.05 |    91.5 |                   
  openrouter.ts    |   84.37 |    33.33 |     100 |   84.37 | 43-48             
  ...outerOAuth.ts |    91.9 |    79.06 |   96.87 |    91.9 | ...53-655,699-701 
 ...ers/thirdParty |     100 |      100 |     100 |     100 |                   
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/commands      |   80.92 |    85.71 |      50 |   80.92 |                   
  auth.ts          |     100 |    83.33 |     100 |     100 | 11,14             
  channel.ts       |   56.66 |      100 |       0 |   56.66 | 15-19,27-34       
  extensions.tsx   |   96.55 |      100 |      50 |   96.55 | 37                
  hooks.tsx        |   66.66 |      100 |       0 |   66.66 | 20-24             
  mcp.ts           |   94.73 |      100 |      50 |   94.73 | 28                
  review.ts        |   51.85 |      100 |       0 |   51.85 | 24-35,38          
 ...mmands/channel |   39.25 |    79.45 |      50 |   39.25 |                   
  ...l-registry.ts |    8.57 |      100 |       0 |    8.57 | 6-21,24-42        
  config-utils.ts  |      92 |      100 |   66.66 |      92 | 21-26             
  configure.ts     |    14.7 |      100 |       0 |    14.7 | 18-21,23-84       
  pairing.ts       |   26.31 |      100 |       0 |   26.31 | ...30,40-50,52-65 
  pidfile.ts       |   96.34 |    86.95 |     100 |   96.34 | 49,59,91          
  start.ts         |   30.98 |       52 |   69.23 |   30.98 | ...72-475,484-486 
  status.ts        |   17.85 |      100 |       0 |   17.85 | 15-26,32-76       
  stop.ts          |      20 |      100 |       0 |      20 | 14-48             
 ...nds/extensions |   84.53 |    88.95 |   81.81 |   84.53 |                   
  consent.ts       |   71.65 |    89.28 |   42.85 |   71.65 | ...85-141,156-162 
  disable.ts       |     100 |      100 |     100 |     100 |                   
  enable.ts        |     100 |      100 |     100 |     100 |                   
  install.ts       |    75.6 |    66.66 |   66.66 |    75.6 | ...39-142,145-153 
  link.ts          |     100 |      100 |     100 |     100 |                   
  list.ts          |     100 |      100 |     100 |     100 |                   
  new.ts           |     100 |      100 |     100 |     100 |                   
  settings.ts      |   99.15 |      100 |   83.33 |   99.15 | 151               
  uninstall.ts     |    37.5 |      100 |   33.33 |    37.5 | 23-45,57-64,67-70 
  update.ts        |   96.32 |      100 |     100 |   96.32 | 101-105           
  utils.ts         |   60.24 |    28.57 |     100 |   60.24 | ...81,83-87,89-93 
 ...les/mcp-server |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-60              
 src/commands/mcp  |   92.29 |    86.08 |   88.88 |   92.29 |                   
  add.ts           |     100 |    98.03 |     100 |     100 | 293               
  list.ts          |   91.22 |    80.76 |      80 |   91.22 | ...19-121,146-147 
  reconnect.ts     |   76.72 |    71.42 |   85.71 |   76.72 | 35-48,153-175     
  remove.ts        |     100 |       80 |     100 |     100 | 21-25             
 ...ommands/review |   11.57 |      100 |       0 |   11.57 |                   
  cleanup.ts       |   17.94 |      100 |       0 |   17.94 | ...01-106,108-109 
  deterministic.ts |   13.75 |      100 |       0 |   13.75 | ...22-738,740-741 
  fetch-pr.ts      |   11.36 |      100 |       0 |   11.36 | ...80-201,203-204 
  load-rules.ts    |   11.32 |      100 |       0 |   11.32 | ...41-153,155-156 
  pr-context.ts    |    6.22 |      100 |       0 |    6.22 | ...97-312,314-315 
  presubmit.ts     |    9.35 |      100 |       0 |    9.35 | ...62-287,289-290 
 ...nds/review/lib |      30 |      100 |       0 |      30 |                   
  gh.ts            |   22.58 |      100 |       0 |   22.58 | ...49,53-54,62-69 
  git.ts           |   22.72 |      100 |       0 |   22.72 | 15-18,29-39,43-44 
  paths.ts         |   52.94 |      100 |       0 |   52.94 | ...26,37-38,42-43 
 src/config        |   92.72 |    85.31 |   85.71 |   92.72 |                   
  auth.ts          |   86.98 |    80.32 |     100 |   86.98 | ...26-227,243-244 
  config.ts        |    88.3 |    85.52 |      76 |    88.3 | ...1713,1737-1738 
  keyBindings.ts   |   96.11 |       50 |     100 |   96.11 | 169-172           
  ...idersScope.ts |      92 |       90 |     100 |      92 | 11-12             
  sandboxConfig.ts |    58.9 |    61.53 |   66.66 |    58.9 | ...54-68,73,77-89 
  settings.ts      |   85.51 |    87.19 |   86.48 |   85.51 | ...1148,1153-1156 
  ...ingsSchema.ts |     100 |      100 |     100 |     100 |                   
  ...tedFolders.ts |   96.22 |       94 |     100 |   96.22 | ...88-190,205-206 
 ...nfig/migration |   94.89 |    78.94 |   83.33 |   94.89 |                   
  index.ts         |   94.87 |    88.88 |     100 |   94.87 | 91-92             
  scheduler.ts     |   96.55 |    77.77 |     100 |   96.55 | 19-20             
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ation/versions |   94.74 |       96 |     100 |   94.74 |                   
  ...-v2-shared.ts |     100 |      100 |     100 |     100 |                   
  v1-to-v2.ts      |   81.75 |    90.19 |     100 |   81.75 | ...28-229,231-247 
  v2-to-v3.ts      |     100 |      100 |     100 |     100 |                   
  v3-to-v4.ts      |     100 |      100 |     100 |     100 |                   
 src/core          |     100 |      100 |     100 |     100 |                   
  auth.ts          |     100 |      100 |     100 |     100 |                   
  initializer.ts   |     100 |      100 |     100 |     100 |                   
  theme.ts         |     100 |      100 |     100 |     100 |                   
 src/dualOutput    |   63.09 |    64.51 |   55.55 |   63.09 |                   
  ...tputBridge.ts |   62.94 |    65.51 |   56.25 |   62.94 | ...22-323,331-334 
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/export        |       0 |        0 |       0 |       0 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-7               
 src/generated     |     100 |      100 |     100 |     100 |                   
  git-commit.ts    |     100 |      100 |     100 |     100 |                   
 src/i18n          |   84.88 |    78.88 |   66.66 |   84.88 |                   
  index.ts         |   70.44 |       74 |   53.84 |   70.44 | ...71-272,282-287 
  languages.ts     |   95.33 |    86.48 |     100 |   95.33 | ...67,195-198,213 
  ...nslateKeys.ts |     100 |      100 |     100 |     100 |                   
  ...lationDict.ts |   93.33 |    66.66 |     100 |   93.33 | 15                
 src/i18n/locales  |     100 |      100 |     100 |     100 |                   
  ca.js            |     100 |      100 |     100 |     100 |                   
  de.js            |     100 |      100 |     100 |     100 |                   
  en.js            |     100 |      100 |     100 |     100 |                   
  fr.js            |     100 |      100 |     100 |     100 |                   
  ja.js            |     100 |      100 |     100 |     100 |                   
  pt.js            |     100 |      100 |     100 |     100 |                   
  ru.js            |     100 |      100 |     100 |     100 |                   
  zh-TW.js         |     100 |      100 |     100 |     100 |                   
  zh.js            |     100 |      100 |     100 |     100 |                   
 ...nonInteractive |   72.75 |    72.14 |   74.07 |   72.75 |                   
  session.ts       |   76.94 |    70.45 |   85.71 |   76.94 | ...80-781,790-800 
  types.ts         |    42.5 |      100 |   33.33 |    42.5 | ...80-581,584-585 
 ...active/control |   77.04 |    88.23 |      80 |   77.04 |                   
  ...rolContext.ts |    7.14 |        0 |       0 |    7.14 | 49-84             
  ...Dispatcher.ts |   91.66 |    91.83 |   88.88 |   91.66 | ...54-372,388,391 
  ...rolService.ts |       8 |        0 |       0 |       8 | 46-179            
 ...ol/controllers |    7.04 |       80 |   13.33 |    7.04 |                   
  ...Controller.ts |   19.32 |      100 |      60 |   19.32 | 81-118,127-210    
  ...Controller.ts |       0 |        0 |       0 |       0 | 1-56              
  ...Controller.ts |    3.96 |      100 |   11.11 |    3.96 | ...61-379,389-494 
  ...Controller.ts |   14.06 |      100 |       0 |   14.06 | ...82-117,130-133 
  ...Controller.ts |     5.2 |      100 |       0 |     5.2 | ...21-433,442-472 
 .../control/types |       0 |        0 |       0 |       0 |                   
  serviceAPIs.ts   |       0 |        0 |       0 |       0 | 1                 
 ...Interactive/io |   97.98 |    93.72 |   95.18 |   97.98 |                   
  ...putAdapter.ts |   97.89 |    92.82 |   98.07 |   97.89 | ...1303,1398-1399 
  ...putAdapter.ts |      96 |    91.66 |   85.71 |      96 | 51-52             
  ...nputReader.ts |     100 |    94.73 |     100 |     100 | 67                
  ...putAdapter.ts |   98.28 |      100 |      90 |   98.28 | 81-82,122-123     
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/patches       |       0 |        0 |       0 |       0 |                   
  is-in-ci.ts      |       0 |        0 |       0 |       0 | 1-17              
 src/remoteInput   |   86.98 |       75 |   85.71 |   86.98 |                   
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  ...putWatcher.ts |   88.12 |    76.08 |   91.66 |   88.12 | ...21-222,233-236 
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/services      |   92.84 |     90.9 |   98.36 |   92.84 |                   
  ...mandLoader.ts |     100 |     92.3 |     100 |     100 | 91                
  ...killLoader.ts |     100 |    96.15 |     100 |     100 | 45                
  ...andService.ts |   98.75 |      100 |     100 |   98.75 | 111               
  ...ionService.ts |   97.19 |    89.77 |     100 |   97.19 | ...85,423-424,428 
  ...mandLoader.ts |   86.83 |    83.87 |     100 |   86.83 | ...30-335,340-345 
  ...omptLoader.ts |   76.05 |    80.64 |   83.33 |   76.05 | ...12-213,279-280 
  ...mandLoader.ts |     100 |      100 |     100 |     100 |                   
  ...nd-factory.ts |    91.5 |    91.66 |     100 |    91.5 | 129,138-145       
  ...ation-tool.ts |     100 |    95.45 |     100 |     100 | 125               
  ...ndMetadata.ts |   98.21 |    96.66 |     100 |   98.21 | 83,87             
  commandUtils.ts  |      96 |    91.66 |     100 |      96 | 48                
  ...and-parser.ts |   90.69 |    85.71 |     100 |   90.69 | 63-66             
  ...ionService.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...ght/generators |    85.9 |    85.61 |   90.47 |    85.9 |                   
  DataProcessor.ts |   85.63 |     85.6 |   92.85 |   85.63 | ...1122,1126-1133 
  ...tGenerator.ts |   98.21 |    85.71 |     100 |   98.21 | 46                
  ...teRenderer.ts |   45.45 |      100 |       0 |   45.45 | 13-51             
 .../insight/types |       0 |       50 |      50 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 | 1                 
 ...mpt-processors |   97.27 |    94.04 |     100 |   97.27 |                   
  ...tProcessor.ts |     100 |      100 |     100 |     100 |                   
  ...eProcessor.ts |   94.52 |    84.21 |     100 |   94.52 | 46-47,93-94       
  ...tionParser.ts |     100 |      100 |     100 |     100 |                   
  ...lProcessor.ts |   97.41 |    95.65 |     100 |   97.41 | 95-98             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services/tips |   97.35 |    83.07 |     100 |   97.35 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  tipHistory.ts    |   92.45 |       70 |     100 |   92.45 | ...22,144,151,160 
  tipRegistry.ts   |     100 |    95.23 |     100 |     100 | 33                
  tipScheduler.ts  |     100 |    91.66 |     100 |     100 | 55                
 src/test-utils    |   93.75 |    83.33 |      80 |   93.75 |                   
  ...omMatchers.ts |   69.69 |       50 |      50 |   69.69 | 32-35,37-39,45-47 
  ...andContext.ts |     100 |      100 |     100 |     100 |                   
  render.tsx       |     100 |      100 |     100 |     100 |                   
 src/ui            |   65.19 |    67.73 |   54.76 |   65.19 |                   
  App.tsx          |     100 |      100 |     100 |     100 |                   
  AppContainer.tsx |   68.02 |     62.4 |      75 |   68.02 | ...2430,2434-2438 
  ...tionNudge.tsx |    9.58 |      100 |       0 |    9.58 | 24-94             
  ...ackDialog.tsx |   29.23 |      100 |       0 |   29.23 | 25-75             
  ...tionNudge.tsx |    7.69 |      100 |       0 |    7.69 | 25-103            
  colors.ts        |   52.72 |      100 |   23.52 |   52.72 | ...52,54-55,60-61 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  keyMatchers.ts   |   95.91 |    96.42 |     100 |   95.91 | 25-26             
  ...tic-colors.ts |     100 |      100 |     100 |     100 |                   
  textConstants.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/auth       |   48.01 |    58.73 |   21.42 |   48.01 |                   
  AuthDialog.tsx   |   64.26 |    44.44 |   16.66 |   64.26 | ...59,366-388,392 
  ...nProgress.tsx |       0 |        0 |       0 |       0 | 1-64              
  ...etupSteps.tsx |    9.61 |      100 |       0 |    9.61 | ...35-352,391-476 
  useAuth.ts       |   76.63 |    68.29 |     100 |   76.63 | ...48,493-499,560 
  ...rSetupFlow.ts |   44.61 |    33.33 |      50 |   44.61 | ...57-378,395-438 
 src/ui/commands   |   70.19 |    79.57 |   80.45 |   70.19 |                   
  aboutCommand.ts  |     100 |    85.71 |     100 |     100 | 36                
  agentsCommand.ts |   83.78 |      100 |      60 |   83.78 | 30-32,42-44       
  ...odeCommand.ts |     100 |      100 |     100 |     100 |                   
  arenaCommand.ts  |   62.81 |    58.73 |   65.21 |   62.81 | ...91-596,681-689 
  authCommand.ts   |     100 |      100 |     100 |     100 |                   
  branchCommand.ts |     100 |      100 |     100 |     100 |                   
  btwCommand.ts    |   95.59 |    71.42 |     100 |   95.59 | 72,154-159        
  bugCommand.ts    |   81.13 |    71.42 |     100 |   81.13 | 60-69             
  clearCommand.ts  |   92.94 |       75 |     100 |   92.94 | 45-46,74-75,93-94 
  ...essCommand.ts |    64.7 |       50 |      75 |    64.7 | ...48-149,163-166 
  ...extCommand.ts |   34.78 |    22.22 |   45.45 |   34.78 | ...86-521,532-533 
  copyCommand.ts   |   98.28 |    94.89 |     100 |   98.28 | ...80,280,321,327 
  deleteCommand.ts |     100 |      100 |     100 |     100 |                   
  diffCommand.ts   |   99.02 |    86.11 |     100 |   99.02 | 222,226           
  ...ryCommand.tsx |   68.09 |    77.77 |   77.77 |   68.09 | ...56-261,315-323 
  docsCommand.ts   |     100 |    88.88 |     100 |     100 | 25                
  doctorCommand.ts |     100 |    93.33 |     100 |     100 | 21                
  dreamCommand.ts  |      75 |    66.66 |   66.66 |      75 | 22-27,44-47       
  editorCommand.ts |     100 |      100 |     100 |     100 |                   
  exportCommand.ts |      60 |    92.85 |   77.77 |      60 | 176-317           
  ...onsCommand.ts |   48.66 |     90.9 |   63.63 |   48.66 | ...05-109,159-211 
  forgetCommand.ts |   26.82 |      100 |      50 |   26.82 | 18-51             
  helpCommand.ts   |     100 |      100 |     100 |     100 |                   
  hooksCommand.ts  |    20.4 |       40 |      40 |    20.4 | ...48-180,204-205 
  ideCommand.ts    |   60.75 |    64.28 |   41.17 |   60.75 | ...05-306,310-324 
  initCommand.ts   |   84.33 |    72.72 |     100 |   84.33 | 68,82-87,89-94    
  ...ghtCommand.ts |   74.56 |    68.42 |     100 |   74.56 | ...31-245,250-273 
  ...ageCommand.ts |   85.76 |    82.82 |     100 |   85.76 | ...51-658,687-694 
  ...elsCommand.ts |     100 |      100 |     100 |     100 |                   
  mcpCommand.ts    |     100 |      100 |     100 |     100 |                   
  memoryCommand.ts |     100 |      100 |     100 |     100 |                   
  modelCommand.ts  |   74.56 |    79.06 |   71.42 |   74.56 | ...91-200,223-228 
  ...onsCommand.ts |     100 |      100 |     100 |     100 |                   
  planCommand.ts   |   78.82 |    76.92 |     100 |   78.82 | 30-35,51-56,68-73 
  quitCommand.ts   |     100 |      100 |     100 |     100 |                   
  recapCommand.ts  |   21.81 |      100 |      50 |   21.81 | 24-73             
  ...berCommand.ts |   32.43 |      100 |      50 |   32.43 | 23-57             
  renameCommand.ts |   85.29 |    77.77 |     100 |   85.29 | ...06-313,320-325 
  ...oreCommand.ts |    92.3 |    87.87 |     100 |    92.3 | ...,83-88,129-130 
  resumeCommand.ts |     100 |      100 |     100 |     100 |                   
  rewindCommand.ts |      80 |      100 |      50 |      80 | 19-21             
  ...ngsCommand.ts |     100 |      100 |     100 |     100 |                   
  ...hubCommand.ts |   81.43 |    65.21 |      80 |   81.43 | ...70-173,176-179 
  skillsCommand.ts |   15.04 |      100 |      25 |   15.04 | ...90-106,109-136 
  statsCommand.ts  |   88.19 |    84.21 |     100 |   88.19 | ...,58-61,143-146 
  ...ineCommand.ts |     100 |      100 |     100 |     100 |                   
  ...aryCommand.ts |    6.46 |      100 |      50 |    6.46 | 31-329            
  tasksCommand.ts  |   77.45 |    73.43 |     100 |   77.45 | ...55-159,181-186 
  ...tupCommand.ts |     100 |      100 |     100 |     100 |                   
  themeCommand.ts  |     100 |      100 |     100 |     100 |                   
  toolsCommand.ts  |     100 |      100 |     100 |     100 |                   
  trustCommand.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  vimCommand.ts    |   54.54 |      100 |      50 |   54.54 | 19-29             
 src/ui/components |   61.31 |    75.66 |   66.66 |   61.31 |                   
  AboutBox.tsx     |     100 |      100 |     100 |     100 |                   
  AnsiOutput.tsx   |   65.57 |      100 |      50 |   65.57 | 69-90             
  ApiKeyInput.tsx  |       0 |        0 |       0 |       0 | 1-97              
  AppHeader.tsx    |   89.39 |       75 |     100 |   89.39 | 35,37-42,44       
  ...odeDialog.tsx |     9.7 |      100 |       0 |     9.7 | 35-47,50-182      
  AsciiArt.ts      |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |   14.63 |      100 |       0 |   14.63 | 18-56             
  ...TextInput.tsx |   77.01 |       76 |     100 |   77.01 | ...20,234-236,263 
  Composer.tsx     |    80.8 |     64.7 |     100 |    80.8 | ...85,103,154,167 
  ...entPrompt.tsx |     100 |      100 |     100 |     100 |                   
  ...ryDisplay.tsx |   75.89 |    62.06 |     100 |   75.89 | ...,88,93-108,113 
  ...geDisplay.tsx |   68.42 |    57.14 |     100 |   68.42 | 16-17,31-32,42-50 
  ...ification.tsx |   28.57 |      100 |       0 |   28.57 | 16-36             
  ...gProfiler.tsx |       0 |        0 |       0 |       0 | 1-36              
  ...ogManager.tsx |   12.53 |      100 |       0 |   12.53 | 63-470            
  ...ngsDialog.tsx |    8.44 |      100 |       0 |    8.44 | 37-195            
  ExitWarning.tsx  |     100 |      100 |     100 |     100 |                   
  ...hProgress.tsx |    87.8 |    33.33 |     100 |    87.8 | 28-31,56          
  ...ustDialog.tsx |     100 |      100 |     100 |     100 |                   
  Footer.tsx       |   79.67 |    58.06 |     100 |   79.67 | ...98-102,104-108 
  ...ngSpinner.tsx |   68.42 |       80 |      50 |   68.42 | 35-52,73,80-81    
  Header.tsx       |   98.62 |    94.28 |     100 |   98.62 | 162,164           
  Help.tsx         |   98.32 |    89.88 |     100 |   98.32 | ...24,381,447-448 
  ...emDisplay.tsx |   63.27 |    36.73 |     100 |   63.27 | ...29-338,341,344 
  ...ngeDialog.tsx |     100 |      100 |     100 |     100 |                   
  InputPrompt.tsx  |   82.25 |    77.43 |   83.33 |   82.25 | ...1347,1412,1462 
  ...Shortcuts.tsx |   20.87 |      100 |       0 |   20.87 | ...6,49-51,67-125 
  ...Indicator.tsx |     100 |    91.42 |     100 |     100 | 65,74             
  ...firmation.tsx |   91.42 |      100 |      50 |   91.42 | 26-31             
  MainContent.tsx  |   81.75 |       75 |     100 |   81.75 | ...70-274,282-286 
  ...elsDialog.tsx |   16.07 |    89.18 |      50 |   16.07 | ...58-159,162-648 
  MemoryDialog.tsx |   53.21 |    51.21 |   57.14 |   53.21 | ...54,366,379-381 
  ...geDisplay.tsx |       0 |        0 |       0 |       0 | 1-41              
  ModelDialog.tsx  |   76.31 |    54.94 |     100 |   76.31 | ...05-521,578-582 
  ...tsDisplay.tsx |     100 |    97.22 |     100 |     100 | 270               
  ...fications.tsx |   18.18 |      100 |       0 |   18.18 | 15-58             
  ...onsDialog.tsx |    2.13 |      100 |       0 |    2.13 | 62-133,148-1004   
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...icePrompt.tsx |   88.14 |    83.87 |     100 |   88.14 | ...01-105,133-138 
  PrepareLabel.tsx |   91.66 |    77.27 |     100 |   91.66 | 73-75,77-79,110   
  ...atePrompt.tsx |    8.57 |      100 |       0 |    8.57 | 24-55,58-134      
  ...geDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ngDisplay.tsx |   21.42 |      100 |       0 |   21.42 | 13-39             
  ...hProgress.tsx |   85.25 |    88.46 |     100 |   85.25 | 121-147           
  ...dSelector.tsx |    4.45 |      100 |       0 |    4.45 | 28-92,100-328     
  ...ionPicker.tsx |      86 |    81.25 |     100 |      86 | ...98-310,344-346 
  ...onPreview.tsx |   92.42 |    84.37 |     100 |   92.42 | ...,70-71,143-145 
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...putPrompt.tsx |   72.56 |       80 |      40 |   72.56 | ...06-109,114-117 
  ...ngsDialog.tsx |   66.88 |    73.52 |     100 |   66.88 | ...11-819,825-826 
  ...ionDialog.tsx |    87.8 |      100 |   33.33 |    87.8 | 36-39,44-51       
  ...putPrompt.tsx |    15.9 |      100 |       0 |    15.9 | 20-63             
  ...Indicator.tsx |   57.14 |      100 |       0 |   57.14 | 12-15             
  ...MoreLines.tsx |      28 |      100 |       0 |      28 | 18-40             
  ...ionPicker.tsx |   17.59 |      100 |       0 |   17.59 | 55-172            
  StatsDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...yTodoList.tsx |   94.17 |       80 |     100 |   94.17 | 56-57,131-134     
  ...nsDisplay.tsx |   87.25 |       64 |     100 |   87.25 | ...45-147,154-156 
  ThemeDialog.tsx  |   89.95 |    46.15 |      75 |   89.95 | ...71-173,243-245 
  Tips.tsx         |   93.54 |       75 |     100 |   93.54 | 39-40             
  TodoDisplay.tsx  |     100 |      100 |     100 |     100 |                   
  ...tsDisplay.tsx |     100 |     87.5 |     100 |     100 | 31-32             
  TrustDialog.tsx  |     100 |    81.81 |     100 |     100 | 71-86             
  ...ification.tsx |   36.36 |      100 |       0 |   36.36 | 15-22             
  ...ackDialog.tsx |    7.84 |      100 |       0 |    7.84 | 24-134            
 ...nts/agent-view |    25.2 |       90 |      10 |    25.2 |                   
  ...atContent.tsx |    8.79 |      100 |       0 |    8.79 | 53-265,271-273    
  ...tChatView.tsx |   21.05 |      100 |       0 |   21.05 | 21-39             
  ...tComposer.tsx |    9.95 |      100 |       0 |    9.95 | 57-308            
  AgentFooter.tsx  |   17.07 |      100 |       0 |   17.07 | 28-66             
  AgentHeader.tsx  |   15.38 |      100 |       0 |   15.38 | 27-64             
  AgentTabBar.tsx  |    8.13 |      100 |       0 |    8.13 | 39-59,64-187      
  ...oryAdapter.ts |     100 |    91.83 |     100 |     100 | 103,109-110,138   
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
 ...mponents/arena |   45.72 |    70.53 |   60.86 |   45.72 |                   
  ArenaCards.tsx   |   73.06 |    71.79 |   85.71 |   73.06 | ...83-185,321-326 
  ...ectDialog.tsx |   83.48 |    69.86 |   88.88 |   83.48 | ...88-392,409-410 
  ...artDialog.tsx |   10.15 |      100 |       0 |   10.15 | 27-161            
  ...tusDialog.tsx |    5.63 |      100 |       0 |    5.63 | 33-75,80-288      
  ...topDialog.tsx |    6.17 |      100 |       0 |    6.17 | 33-213            
 ...ackground-view |   75.44 |     83.6 |   85.29 |   75.44 |                   
  ...sksDialog.tsx |   70.05 |       79 |   76.19 |   70.05 | ...1119,1195-1197 
  ...TasksPill.tsx |   70.83 |    86.95 |     100 |   70.83 | 44,84-96,104-112  
  ...gentPanel.tsx |   99.52 |    93.18 |     100 |   99.52 | 123               
 ...nts/extensions |   45.28 |    33.33 |      60 |   45.28 |                   
  ...gerDialog.tsx |   44.31 |    34.14 |      75 |   44.31 | ...71-480,483-488 
  index.ts         |       0 |        0 |       0 |       0 | 1-9               
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...tensions/steps |   54.77 |    94.23 |   66.66 |   54.77 |                   
  ...ctionStep.tsx |   95.12 |    92.85 |   85.71 |   95.12 | 84-86,89          
  ...etailStep.tsx |    6.18 |      100 |       0 |    6.18 | 17-128            
  ...nListStep.tsx |   88.35 |    94.73 |      80 |   88.35 | 51-52,58-71,105   
  ...electStep.tsx |   13.46 |      100 |       0 |   13.46 | 20-70             
  ...nfirmStep.tsx |   19.56 |      100 |       0 |   19.56 | 23-65             
  index.ts         |     100 |      100 |     100 |     100 |                   
 ...mponents/hooks |   72.24 |    70.52 |      80 |   72.24 |                   
  ...etailStep.tsx |   96.52 |       75 |     100 |   96.52 | 33,37,50,59       
  ...etailStep.tsx |   93.27 |    73.68 |     100 |   93.27 | 41-42,99-104,110  
  ...abledStep.tsx |     100 |      100 |     100 |     100 |                   
  ...sListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entDialog.tsx |   36.09 |    47.05 |      50 |   36.09 | ...49,453-466,470 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-13              
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...components/mcp |   20.83 |    83.72 |   83.33 |   20.83 |                   
  ...ealthPill.tsx |   68.42 |    85.71 |     100 |   68.42 | 40-46             
  ...entDialog.tsx |    3.64 |      100 |       0 |    3.64 | 41-717            
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-30              
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   94.79 |    85.71 |     100 |   94.79 | 16,20,35,109-110  
 ...ents/mcp/steps |    6.65 |      100 |       0 |    6.65 |                   
  ...icateStep.tsx |     5.1 |      100 |       0 |     5.1 | 34-95,98-334      
  ...electStep.tsx |   10.95 |      100 |       0 |   10.95 | 16-88             
  ...etailStep.tsx |    5.26 |      100 |       0 |    5.26 | 31-247            
  ...rListStep.tsx |    5.88 |      100 |       0 |    5.88 | 20-176            
  ...etailStep.tsx |   10.41 |      100 |       0 |   10.41 | ...1,67-79,82-139 
  ToolListStep.tsx |    7.14 |      100 |       0 |    7.14 | 16-146            
 ...nents/messages |   82.15 |    80.23 |   72.85 |   82.15 |                   
  ...ionDialog.tsx |   77.35 |    74.54 |    62.5 |   77.35 | ...90,508,526-528 
  BtwMessage.tsx   |     100 |      100 |     100 |     100 |                   
  ...upDisplay.tsx |   97.67 |    83.72 |     100 |   97.67 | 119,142,150       
  ...onMessage.tsx |   91.93 |    82.35 |     100 |   91.93 | 57-59,61,63       
  ...nMessages.tsx |   79.06 |      100 |      70 |   79.06 | ...51-264,268-280 
  DiffRenderer.tsx |   93.19 |    86.17 |     100 |   93.19 | ...09,237-238,304 
  ...tsDisplay.tsx |   97.82 |    77.27 |     100 |   97.82 | 87,89             
  ...ssMessage.tsx |    12.5 |      100 |       0 |    12.5 | 18-59             
  ...edMessage.tsx |   16.66 |      100 |       0 |   16.66 | 22-38             
  ...sMessages.tsx |   55.67 |       40 |   28.57 |   55.67 | ...20-125,133-145 
  ...ryMessage.tsx |   14.28 |      100 |       0 |   14.28 | 23-62             
  ...onMessage.tsx |   81.02 |    69.23 |   33.33 |   81.02 | ...24-426,433-435 
  ...upMessage.tsx |      84 |    93.61 |     100 |      84 | ...56-383,405-420 
  ToolMessage.tsx  |   88.84 |    75.71 |    92.3 |   88.84 | ...44-749,776-778 
 ...ponents/shared |   82.37 |    77.36 |   92.75 |   82.37 |                   
  ...ctionList.tsx |   99.03 |    95.65 |     100 |   99.03 | 85                
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  EnumSelector.tsx |     100 |    96.42 |     100 |     100 | 58                
  MaxSizedBox.tsx  |   83.01 |    86.25 |   88.88 |   83.01 | ...12-513,618-619 
  MultiSelect.tsx  |    6.29 |      100 |       0 |    6.29 | 35-42,45-176      
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  ...eSelector.tsx |     100 |       60 |     100 |     100 | 40-45             
  TextInput.tsx    |   72.98 |    55.55 |      80 |   72.98 | ...08-212,224-230 
  ...apsedTime.tsx |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |     100 |      100 |     100 |     100 |                   
  text-buffer.ts   |   83.62 |    75.62 |   97.61 |   83.62 | ...2272,2300,2368 
  ...er-actions.ts |   86.71 |    67.79 |     100 |   86.71 | ...07-608,809-811 
 ...ents/subagents |   30.87 |        0 |       0 |   30.87 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
  reducers.tsx     |    12.1 |      100 |       0 |    12.1 | 33-190            
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   10.95 |      100 |       0 |   10.95 | ...1,56-57,60-102 
 ...bagents/create |    9.13 |      100 |       0 |    9.13 |                   
  ...ionWizard.tsx |    7.28 |      100 |       0 |    7.28 | 34-299            
  ...rSelector.tsx |   14.75 |      100 |       0 |   14.75 | 26-85             
  ...onSummary.tsx |    4.26 |      100 |       0 |    4.26 | 27-331            
  ...tionInput.tsx |    8.63 |      100 |       0 |    8.63 | 23-177            
  ...dSelector.tsx |   33.33 |      100 |       0 |   33.33 | 20-21,26-27,36-63 
  ...nSelector.tsx |    37.5 |      100 |       0 |    37.5 | 20-21,26-27,36-58 
  ...EntryStep.tsx |   12.76 |      100 |       0 |   12.76 | 34-78             
  ToolSelector.tsx |    4.16 |      100 |       0 |    4.16 | 31-253            
 ...bagents/manage |    8.39 |      100 |       0 |    8.39 |                   
  ...ctionStep.tsx |   10.25 |      100 |       0 |   10.25 | 21-103            
  ...eleteStep.tsx |   20.93 |      100 |       0 |   20.93 | 23-62             
  ...tEditStep.tsx |   25.53 |      100 |       0 |   25.53 | ...2,37-38,51-124 
  ...ctionStep.tsx |    2.29 |      100 |       0 |    2.29 | 28-449            
  ...iewerStep.tsx |   13.72 |      100 |       0 |   13.72 | 18-73             
  ...gerDialog.tsx |    6.74 |      100 |       0 |    6.74 | 35-341            
 ...mponents/views |   42.16 |    69.23 |   21.42 |   42.16 |                   
  ContextUsage.tsx |     4.7 |      100 |       0 |     4.7 | ...52-167,170-456 
  DoctorReport.tsx |     9.8 |      100 |       0 |     9.8 | 25-54,57-131      
  ...sionsList.tsx |   87.69 |    73.68 |     100 |   87.69 | 65-72             
  McpStatus.tsx    |   89.53 |    60.52 |     100 |   89.53 | ...72,175-177,262 
  SkillsList.tsx   |   27.27 |      100 |       0 |   27.27 | 18-35             
  ToolsList.tsx    |     100 |      100 |     100 |     100 |                   
 src/ui/contexts   |   77.05 |    78.24 |   82.14 |   77.05 |                   
  ...ewContext.tsx |   65.77 |      100 |      75 |   65.77 | ...22-225,231-241 
  AppContext.tsx   |      80 |       50 |     100 |      80 | 19-20             
  ...ewContext.tsx |   93.37 |    68.57 |      50 |   93.37 | ...94-195,222-226 
  ...deContext.tsx |     100 |      100 |     100 |     100 |                   
  ...igContext.tsx |   81.81 |       50 |     100 |   81.81 | 15-16             
  ...ssContext.tsx |   81.88 |    82.26 |     100 |   81.88 | ...1153,1159-1161 
  ...owContext.tsx |   89.28 |       80 |   66.66 |   89.28 | 34,47-48,60-62    
  ...deContext.tsx |     100 |      100 |      50 |     100 |                   
  ...onContext.tsx |   43.28 |     62.5 |    62.5 |   43.28 | ...56-259,263-266 
  ...gsContext.tsx |   83.33 |       50 |     100 |   83.33 | 17-18             
  ...usContext.tsx |     100 |      100 |     100 |     100 |                   
  ...ngContext.tsx |   71.42 |       50 |     100 |   71.42 | 17-20             
  ...utContext.tsx |   85.71 |      100 |   66.66 |   85.71 | 13-14             
  ...nsContext.tsx |   88.23 |       50 |     100 |   88.23 | 108-109           
  ...teContext.tsx |   86.66 |       50 |     100 |   86.66 | 173-174           
  ...deContext.tsx |   76.08 |    72.72 |     100 |   76.08 | 47-48,52-59,77-78 
 src/ui/editors    |   93.33 |    85.71 |   66.66 |   93.33 |                   
  ...ngsManager.ts |   93.33 |    85.71 |   66.66 |   93.33 | 49,63-64          
 src/ui/hooks      |   81.33 |    81.11 |   86.47 |   81.33 |                   
  ...dProcessor.ts |   83.12 |    82.56 |     100 |   83.12 | ...88-389,408-435 
  keyToAnsi.ts     |    3.92 |      100 |       0 |    3.92 | 19-77             
  ...dProcessor.ts |    94.8 |    70.58 |     100 |    94.8 | ...76-277,282-283 
  ...dProcessor.ts |    75.9 |    63.44 |   61.53 |    75.9 | ...84,908,927-931 
  ...amingState.ts |   12.22 |      100 |       0 |   12.22 | 54-158            
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...ationFrame.ts |      32 |       60 |     100 |      32 | 42-44,51-90       
  ...odeCommand.ts |   58.82 |      100 |     100 |   58.82 | 28,33-48          
  ...enaCommand.ts |      85 |      100 |     100 |      85 | 23-24,29          
  ...aInProcess.ts |   19.81 |    66.66 |      25 |   19.81 | 57-175            
  ...Completion.ts |   92.77 |    89.09 |     100 |   92.77 | ...86-187,220-223 
  ...ifications.ts |   92.07 |    96.29 |     100 |   92.07 | 116-124           
  ...tIndicator.ts |     100 |    93.75 |     100 |     100 | 63                
  ...waySummary.ts |   96.22 |    69.69 |     100 |   96.22 | 125-127,169       
  ...ndTaskView.ts |   94.11 |    76.92 |     100 |   94.11 | 119-123,216,222   
  ...ketedPaste.ts |    23.8 |      100 |       0 |    23.8 | 19-37             
  ...nchCommand.ts |   93.75 |    73.17 |     100 |   93.75 | ...68-169,221-222 
  ...ompletion.tsx |   95.95 |    82.75 |     100 |   95.95 | ...22-223,225-226 
  ...dMigration.ts |   90.62 |       75 |     100 |   90.62 | 38-40             
  useCompletion.ts |    92.4 |     87.5 |     100 |    92.4 | 68-69,93-94,98-99 
  ...nitMessage.ts |     100 |      100 |     100 |     100 |                   
  ...extualTips.ts |   76.92 |       50 |     100 |   76.92 | 55,68,71-75,88-96 
  ...eteCommand.ts |   33.33 |       50 |     100 |   33.33 | 30,34,41-90       
  ...ialogClose.ts |   16.66 |      100 |     100 |   16.66 | 79-139            
  ...oublePress.ts |   53.12 |       75 |     100 |   53.12 | 33-35,41-54       
  ...orSettings.ts |     100 |      100 |     100 |     100 |                   
  ...Completion.ts |   99.12 |     97.7 |     100 |   99.12 | 182-183           
  ...ionUpdates.ts |   93.45 |     92.3 |     100 |   93.45 | ...83-287,300-306 
  ...agerDialog.ts |   88.88 |      100 |     100 |   88.88 | 21,25             
  ...backDialog.ts |   54.47 |       50 |   33.33 |   54.47 | ...69-171,193-194 
  useFocus.ts      |     100 |      100 |     100 |     100 |                   
  ...olderTrust.ts |     100 |      100 |     100 |     100 |                   
  ...ggestions.tsx |   89.15 |     62.5 |      50 |   89.15 | ...22-124,149-150 
  ...miniStream.ts |   75.92 |    72.95 |   91.66 |   75.92 | ...2300,2313-2321 
  ...BranchName.ts |    90.9 |     92.3 |     100 |    90.9 | 19-20,55-58       
  ...oryManager.ts |   93.15 |    93.75 |     100 |   93.15 | 44,107-110        
  ...ooksDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...stListener.ts |     100 |      100 |     100 |     100 |                   
  ...nAuthError.ts |   76.19 |       50 |     100 |   76.19 | 39-40,43-45       
  ...putHistory.ts |   92.59 |    85.71 |     100 |   92.59 | 63-64,72,94-96    
  ...storyStore.ts |     100 |    94.11 |     100 |     100 | 69                
  useKeypress.ts   |     100 |      100 |     100 |     100 |                   
  ...rdProtocol.ts |   36.36 |      100 |       0 |   36.36 | 24-31             
  ...unchEditor.ts |    9.67 |      100 |       0 |    9.67 | 11-32,39-90       
  ...gIndicator.ts |     100 |      100 |     100 |     100 |                   
  useLogger.ts     |   21.05 |      100 |       0 |   21.05 | 15-37             
  useMCPHealth.ts  |   63.15 |       75 |      50 |   63.15 | 42-52,64-67       
  ...elsCommand.ts |     100 |      100 |     100 |     100 |                   
  useMcpDialog.ts  |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...moryDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...oryMonitor.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...delCommand.ts |     100 |       75 |     100 |     100 | 22                
  ...raseCycler.ts |   84.74 |    76.47 |     100 |   84.74 | ...49,52-53,69-71 
  ...derUpdates.ts |   86.38 |    77.19 |     100 |   86.38 | ...22,281-293,341 
  useQwenAuth.ts   |     100 |      100 |     100 |     100 |                   
  ...lScheduler.ts |    84.7 |    93.33 |     100 |    84.7 | ...71-276,372-382 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-7               
  ...umeCommand.ts |   97.24 |    76.92 |     100 |   97.24 | 104-105,145       
  ...ompletion.tsx |   90.59 |    83.33 |     100 |   90.59 | ...01,104,137-140 
  ...ectionList.ts |   96.96 |    95.69 |     100 |   96.96 | ...82-183,237-240 
  ...sionPicker.ts |   79.79 |    61.19 |     100 |   79.79 | ...02-404,413-415 
  ...earchInput.ts |     100 |      100 |     100 |     100 |                   
  ...ngsCommand.ts |   18.75 |      100 |       0 |   18.75 | 10-25             
  ...ellHistory.ts |   91.74 |    79.41 |     100 |   91.74 | ...74,122-123,133 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-73              
  ...Completion.ts |   82.67 |    85.41 |   94.73 |   82.67 | ...68-670,678-714 
  ...tateAndRef.ts |     100 |      100 |     100 |     100 |                   
  useStatusLine.ts |     100 |    98.79 |     100 |     100 | 257               
  ...eateDialog.ts |   88.23 |      100 |     100 |   88.23 | 14,18             
  ...tification.ts |     100 |    85.71 |     100 |     100 | 47                
  ...alProgress.ts |   53.06 |       50 |   66.66 |   53.06 | ...53,61-68,79-85 
  ...rminalSize.ts |   76.19 |      100 |      50 |   76.19 | 21-25             
  ...emeCommand.ts |   67.01 |    29.41 |     100 |   67.01 | ...10-111,115-116 
  useTimer.ts      |   88.09 |    85.71 |     100 |   88.09 | 44-45,51-53       
  ...lMigration.ts |       0 |        0 |       0 |       0 |                   
  ...rustModify.ts |     100 |      100 |     100 |     100 |                   
  ...elcomeBack.ts |   87.36 |     90.9 |     100 |   87.36 | ...,94-96,114-115 
  vim.ts           |   83.77 |    80.31 |     100 |   83.77 | ...55,759-767,776 
 src/ui/layouts    |   89.72 |     87.5 |     100 |   89.72 |                   
  ...AppLayout.tsx |   89.88 |     87.5 |     100 |   89.88 | 51-53,93-98       
  ...AppLayout.tsx |   89.47 |     87.5 |     100 |   89.47 | 58-63             
 ...i/manageModels |   93.61 |       48 |     100 |   93.61 |                   
  manageModels.ts  |   93.61 |       48 |     100 |   93.61 | ...63-166,179,209 
 src/ui/models     |   80.24 |    79.16 |   71.42 |   80.24 |                   
  ...ableModels.ts |   80.24 |    79.16 |   71.42 |   80.24 | ...,61-71,123-125 
 ...noninteractive |     100 |      100 |    7.14 |     100 |                   
  ...eractiveUi.ts |     100 |      100 |    7.14 |     100 |                   
 src/ui/state      |   94.91 |    81.81 |     100 |   94.91 |                   
  extensions.ts    |   94.91 |    81.81 |     100 |   94.91 | 68-69,88          
 src/ui/themes     |   98.53 |    70.58 |     100 |   98.53 |                   
  ansi-light.ts    |     100 |      100 |     100 |     100 |                   
  ansi.ts          |     100 |      100 |     100 |     100 |                   
  atom-one-dark.ts |     100 |      100 |     100 |     100 |                   
  ayu-light.ts     |     100 |      100 |     100 |     100 |                   
  ayu.ts           |     100 |      100 |     100 |     100 |                   
  color-utils.ts   |     100 |      100 |     100 |     100 |                   
  default-light.ts |     100 |      100 |     100 |     100 |                   
  default.ts       |     100 |      100 |     100 |     100 |                   
  ...inal-theme.ts |   88.59 |    85.96 |     100 |   88.59 | ...57-261,266-270 
  dracula.ts       |     100 |      100 |     100 |     100 |                   
  github-dark.ts   |     100 |      100 |     100 |     100 |                   
  github-light.ts  |     100 |      100 |     100 |     100 |                   
  googlecode.ts    |     100 |      100 |     100 |     100 |                   
  no-color.ts      |     100 |      100 |     100 |     100 |                   
  qwen-dark.ts     |     100 |      100 |     100 |     100 |                   
  qwen-light.ts    |     100 |      100 |     100 |     100 |                   
  ...tic-tokens.ts |     100 |      100 |     100 |     100 |                   
  ...-of-purple.ts |     100 |      100 |     100 |     100 |                   
  theme-manager.ts |   87.98 |    82.89 |     100 |   87.98 | ...48-357,362-363 
  theme.ts         |     100 |    38.02 |     100 |     100 | ...34-449,457-461 
  xcode.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/utils      |   83.06 |    81.92 |   91.91 |   83.06 |                   
  ...Colorizer.tsx |   82.78 |    88.23 |     100 |   82.78 | ...10-111,197-223 
  ...nRenderer.tsx |   57.89 |    55.31 |      50 |   57.89 | ...86-188,208-227 
  ...wnDisplay.tsx |   86.01 |    87.41 |     100 |   86.01 | ...87,704,729-754 
  ...idDiagram.tsx |   87.79 |    95.34 |     100 |   87.79 | 156-179           
  ...eRenderer.tsx |   93.18 |    81.43 |      95 |   93.18 | ...20-623,667-672 
  ...dWorkUtils.ts |     100 |      100 |     100 |     100 |                   
  ...boardUtils.ts |   59.61 |    58.82 |     100 |   59.61 | ...,86-88,107-149 
  commandUtils.ts  |    95.9 |    88.29 |     100 |    95.9 | ...62,164-165,289 
  computeStats.ts  |     100 |      100 |     100 |     100 |                   
  customBanner.ts  |   90.68 |    91.22 |     100 |   90.68 | ...13,324-327,334 
  displayUtils.ts  |   88.37 |    72.22 |     100 |   88.37 | 23,25,29,31,33    
  formatters.ts    |   95.23 |    98.27 |     100 |   95.23 | 117-120           
  gradientUtils.ts |     100 |      100 |     100 |     100 |                   
  highlight.ts     |     100 |      100 |     100 |     100 |                   
  ...oryMapping.ts |     100 |    94.28 |     100 |     100 | 29,51             
  isNarrowWidth.ts |     100 |      100 |     100 |     100 |                   
  ...olDetector.ts |    8.23 |      100 |       0 |    8.23 | ...31-132,135-136 
  latexRenderer.ts |   94.95 |     73.8 |     100 |   94.95 | ...76-178,184-187 
  layoutUtils.ts   |     100 |      100 |     100 |     100 |                   
  ...nUtilities.ts |   69.84 |    85.71 |     100 |   69.84 | 75-91,100-101     
  ...ToolGroups.ts |   98.66 |    96.77 |     100 |   98.66 | 48-49             
  ...geRenderer.ts |   86.23 |    69.06 |   95.12 |   86.23 | ...1284,1324-1330 
  ...alRenderer.ts |   86.69 |     71.9 |     100 |   86.69 | ...1476,1513-1519 
  ...lsBySource.ts |     100 |    95.23 |     100 |     100 | 84                
  ...mConstants.ts |     100 |      100 |     100 |     100 |                   
  ...storyUtils.ts |   61.06 |    69.62 |      90 |   61.06 | ...64,412,417-439 
  ...ickerUtils.ts |     100 |      100 |     100 |     100 |                   
  ...izedOutput.ts |   94.94 |      100 |   88.88 |   94.94 | 112-117           
  ...wOptimizer.ts |     100 |    96.77 |     100 |     100 | 69                
  terminalSetup.ts |    4.37 |      100 |       0 |    4.37 | 44-393            
  textUtils.ts     |   97.35 |    94.38 |   91.66 |   97.35 | ...50-251,386-387 
  todoSnapshot.ts  |   89.11 |    93.33 |     100 |   89.11 | ...,66-78,180-181 
  updateCheck.ts   |     100 |    80.95 |     100 |     100 | 30-42             
 ...i/utils/export |   56.77 |     40.8 |   79.41 |   56.77 |                   
  collect.ts       |   55.92 |    50.58 |   86.36 |   55.92 | ...25-640,642-647 
  index.ts         |     100 |      100 |     100 |     100 |                   
  normalize.ts     |   57.47 |    20.51 |      80 |   57.47 | ...09-310,324-359 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
  utils.ts         |      40 |      100 |       0 |      40 | 11-13             
 ...ort/formatters |    3.38 |      100 |       0 |    3.38 |                   
  html.ts          |    9.61 |      100 |       0 |    9.61 | ...28,34-76,82-84 
  json.ts          |      50 |      100 |       0 |      50 | 14-15             
  jsonl.ts         |     3.5 |      100 |       0 |     3.5 | 14-76             
  markdown.ts      |    0.94 |      100 |       0 |    0.94 | 13-295            
 src/utils         |   73.31 |       90 |   93.68 |   73.31 |                   
  acpModelUtils.ts |     100 |      100 |     100 |     100 |                   
  apiPreconnect.ts |   96.52 |    97.05 |     100 |   96.52 | 164-167           
  checks.ts        |   33.33 |      100 |       0 |   33.33 | 23-28             
  cleanup.ts       |   84.12 |    93.33 |      80 |   84.12 | 75,106-115        
  commands.ts      |     100 |      100 |     100 |     100 |                   
  commentJson.ts   |   87.17 |    90.47 |     100 |   87.17 | 64-73             
  ...Calculator.ts |     100 |      100 |     100 |     100 |                   
  deepMerge.ts     |     100 |       90 |     100 |     100 | 41-43,49          
  ...ScopeUtils.ts |   97.56 |    88.88 |     100 |   97.56 | 67                
  doctorChecks.ts  |   71.06 |       75 |     100 |   71.06 | ...95-301,325-341 
  ...putCapture.ts |   90.65 |    86.17 |     100 |   90.65 | ...72,370,372-373 
  ...arResolver.ts |   94.28 |       88 |     100 |   94.28 | 28-29,125-126     
  errors.ts        |   98.67 |    96.36 |     100 |   98.67 | 67-68             
  events.ts        |     100 |      100 |     100 |     100 |                   
  gitUtils.ts      |   91.91 |    84.61 |     100 |   91.91 | 78-81,124-127     
  ...AutoUpdate.ts |   90.76 |    93.33 |   88.88 |   90.76 | 103-114           
  ...lationInfo.ts |     100 |      100 |     100 |     100 |                   
  languageUtils.ts |   97.89 |    96.42 |     100 |   97.89 | 132-133           
  math.ts          |       0 |        0 |       0 |       0 | 1-15              
  ...onfigUtils.ts |     100 |      100 |     100 |     100 |                   
  ...iveHelpers.ts |   96.82 |    93.28 |     100 |   96.82 | ...84-485,583,596 
  osc.ts           |    97.5 |      100 |   88.88 |    97.5 | 195-196           
  package.ts       |   88.88 |       80 |     100 |   88.88 | 33-34             
  processUtils.ts  |     100 |      100 |     100 |     100 |                   
  readStdin.ts     |   79.62 |       90 |      80 |   79.62 | 33-40,52-54       
  relaunch.ts      |   98.07 |    76.92 |     100 |   98.07 | 70                
  resolvePath.ts   |   66.66 |       25 |     100 |   66.66 | 12-13,16,18-19    
  sandbox.ts       |       0 |        0 |       0 |       0 | 1-1047            
  settingsUtils.ts |   82.89 |    90.67 |   89.47 |   82.89 | ...52-663,670-678 
  spawnWrapper.ts  |     100 |      100 |     100 |     100 |                   
  ...upProfiler.ts |     100 |       96 |     100 |     100 | 110               
  ...upWarnings.ts |     100 |      100 |     100 |     100 |                   
  stdioHelpers.ts  |     100 |       60 |     100 |     100 | 23,32             
  systemInfo.ts    |   92.52 |     90.9 |   83.33 |   92.52 | 63-69,184         
  ...InfoFields.ts |    87.5 |     64.1 |     100 |    87.5 | ...21-122,143-144 
  ...iffPreview.ts |   94.11 |    83.33 |     100 |   94.11 | 13                
  ...entEmitter.ts |     100 |      100 |     100 |     100 |                   
  ...upWarnings.ts |   91.17 |    82.35 |     100 |   91.17 | 67-68,73-74,77-78 
  version.ts       |     100 |       50 |     100 |     100 | 11                
  windowTitle.ts   |     100 |      100 |     100 |     100 |                   
  ...WithBackup.ts |   63.15 |    81.25 |     100 |   63.15 | 93,118-157        
-------------------|---------|----------|---------|---------|-------------------
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   78.24 |     82.6 |   80.83 |   78.24 |                   
 src               |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/__mocks__/fs  |       0 |        0 |       0 |       0 |                   
  promises.ts      |       0 |        0 |       0 |       0 | 1-48              
 src/agents        |   86.11 |    76.88 |   91.66 |   86.11 |                   
  ...transcript.ts |   88.92 |    76.66 |     100 |   88.92 | ...82,306-307,438 
  ...ent-resume.ts |   81.23 |    69.89 |   77.41 |   81.23 | ...1021,1024-1026 
  ...ound-tasks.ts |   95.13 |    86.61 |     100 |   95.13 | ...06-707,733-734 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/arena  |   76.98 |    67.72 |   78.72 |   76.98 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.92 |    64.19 |   78.26 |   75.92 | ...1860,1866-1867 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    73.46 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   76.29 |    86.15 |   73.04 |   76.29 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |   91.25 |    90.62 |   86.66 |   91.25 | ...94,249-269,328 
  TmuxBackend.ts   |    90.7 |    76.55 |   97.36 |    90.7 | ...87,697,743-747 
  detect.ts        |   31.25 |      100 |       0 |   31.25 | 34-88             
  index.ts         |     100 |      100 |     100 |     100 |                   
  iterm-it2.ts     |     100 |     92.1 |     100 |     100 | 37-38,106         
  tmux-commands.ts |    6.64 |      100 |    3.03 |    6.64 | ...93-363,386-503 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...agents/runtime |   81.13 |     76.7 |   71.42 |   81.13 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   76.45 |    72.35 |   60.86 |   76.45 | ...1604,1631-1677 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   81.19 |    71.73 |   60.86 |   81.19 | ...98-399,402-403 
  ...nteractive.ts |   79.71 |    79.62 |      75 |   79.71 | ...54,456,458,461 
  ...statistics.ts |   98.19 |    82.35 |     100 |   98.19 | 127,151,192,225   
  agent-types.ts   |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/config        |   75.73 |    78.23 |   62.08 |   75.73 |                   
  config.ts        |   73.51 |    75.93 |   56.93 |   73.51 | ...3084,3095-3107 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   95.07 |    93.44 |   89.47 |   95.07 | ...66-267,270-271 
 ...nfirmation-bus |   98.29 |    97.14 |     100 |   98.29 |                   
  message-bus.ts   |   98.14 |    97.05 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/core          |    84.2 |    82.22 |   88.83 |    84.2 |                   
  baseLlmClient.ts |   91.63 |    84.37 |   84.61 |   91.63 | ...91,299-313,380 
  client.ts        |   78.24 |    77.11 |   85.18 |   78.24 | ...1478,1515-1518 
  ...tGenerator.ts |    72.1 |    61.11 |     100 |    72.1 | ...63,365,372-375 
  ...lScheduler.ts |   81.13 |    82.25 |   93.33 |   81.13 | ...2332,2384-2388 
  geminiChat.ts    |   88.81 |    84.36 |    87.5 |   88.81 | ...1304,1371-1372 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | 34-42,45-49,52-87 
  logger.ts        |   82.25 |    81.81 |     100 |   82.25 | ...57-361,407-421 
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   92.59 |       75 |      50 |   92.59 | 41-42             
  ...on-helpers.ts |   85.71 |    70.58 |     100 |   85.71 | ...90-191,205-214 
  ...issionFlow.ts |   98.59 |    94.73 |     100 |   98.59 | 93                
  prompts.ts       |   89.16 |    86.41 |   76.92 |   89.16 | ...-965,1168-1169 
  tokenLimits.ts   |     100 |    89.47 |     100 |     100 | 51-52             
  ...okTriggers.ts |   99.31 |    90.41 |     100 |   99.31 | 124,135           
  turn.ts          |   96.42 |    88.88 |     100 |   96.42 | ...00,413-414,462 
 ...ntentGenerator |   95.12 |    81.91 |   93.61 |   95.12 |                   
  ...tGenerator.ts |   97.13 |    83.58 |    92.3 |   97.13 | ...22,714,870,926 
  converter.ts     |   94.51 |    80.62 |     100 |   94.51 | ...06-607,617,816 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
 ...ntentGenerator |   91.53 |    71.64 |   93.33 |   91.53 |                   
  ...tGenerator.ts |      90 |    70.96 |   92.85 |      90 | ...80-286,304-305 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |   93.33 |    83.33 |      90 |   93.33 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   93.31 |    83.33 |      90 |   93.31 | ...94,804-805,833 
 ...ntentGenerator |   80.52 |    84.56 |   89.61 |   80.52 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   76.76 |    82.08 |    87.5 |   76.76 | ...1575,1596-1602 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   52.38 |    44.44 |      50 |   52.38 | ...77,81-85,89-93 
  ...tGenerator.ts |   48.78 |    91.66 |   77.77 |   48.78 | ...10-163,166-167 
  pipeline.ts      |   93.65 |     84.9 |     100 |   93.65 | ...79-480,488,553 
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |   90.66 |    88.57 |     100 |   90.66 | ...15-319,349-350 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   96.56 |    88.46 |   95.45 |   96.56 |                   
  dashscope.ts     |   97.02 |    88.15 |   93.33 |   97.02 | ...37-238,314-315 
  deepseek.ts      |   95.55 |    90.56 |     100 |   95.55 | ...31-132,145-146 
  default.ts       |   94.62 |    86.36 |   85.71 |   94.62 | 85-86,156-158     
  index.ts         |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  mistral.ts       |   96.07 |    73.33 |     100 |   96.07 | 32-33             
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
 src/extension     |   60.56 |    79.46 |    78.4 |   60.56 |                   
  ...-converter.ts |   62.35 |    47.82 |      90 |   62.35 | ...90-791,800-832 
  ...ionManager.ts |   47.04 |    82.06 |    65.9 |   47.04 | ...1398,1408-1427 
  ...onSettings.ts |   93.46 |    93.05 |     100 |   93.46 | ...17-221,228-232 
  ...-converter.ts |   54.88 |    94.44 |      60 |   54.88 | ...35-146,158-192 
  github.ts        |   44.94 |    88.52 |      60 |   44.94 | ...53-359,398-451 
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   97.29 |    93.75 |     100 |   97.29 | ...64,184-185,274 
  npm.ts           |   48.66 |    76.08 |      75 |   48.66 | ...18-420,427-431 
  override.ts      |   94.11 |    88.88 |     100 |   94.11 | 63-64,81-82       
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-108,143-149    
  storage.ts       |     100 |      100 |     100 |     100 |                   
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.75 |    83.33 |     100 |   88.75 | ...28-231,234-237 
 src/followup      |   46.91 |     92.3 |   71.87 |   46.91 |                   
  followupState.ts |      96 |    89.74 |     100 |      96 | 159-161,218-219   
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   95.06 |       84 |     100 |   95.06 | 78,108,122,133    
  speculation.ts   |   13.22 |      100 |   16.66 |   13.22 | 88-458,518-568    
  ...onToolGate.ts |     100 |    96.29 |     100 |     100 | 93                
  ...nGenerator.ts |    38.4 |    95.12 |   33.33 |    38.4 | ...16-318,353-383 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/hooks         |   80.63 |    84.35 |   84.16 |   80.63 |                   
  ...okRegistry.ts |   86.48 |    77.08 |     100 |   86.48 | ...41-344,362-369 
  ...bortSignal.ts |     100 |      100 |     100 |     100 |                   
  ...terpolator.ts |   96.66 |    93.33 |     100 |   96.66 | 66-67             
  ...HookRunner.ts |   96.68 |    87.23 |     100 |   96.68 | 110-112,231-233   
  ...Aggregator.ts |   96.37 |    90.54 |     100 |   96.37 | ...89,291-292,365 
  ...entHandler.ts |   95.58 |    84.37 |   92.59 |   95.58 | ...29,682-683,693 
  hookPlanner.ts   |   84.13 |    76.59 |      90 |   84.13 | ...38,144,162-173 
  hookRegistry.ts  |   88.83 |    86.36 |     100 |   88.83 | ...21,326,330,334 
  hookRunner.ts    |   53.94 |     72.6 |   61.11 |   53.94 | ...27-728,737-738 
  hookSystem.ts    |   75.47 |      100 |   56.41 |   75.47 | ...75-576,582-583 
  ...HookRunner.ts |   75.51 |     61.9 |      80 |   75.51 | ...05-406,424-425 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |    96.5 |     91.8 |     100 |    96.5 | ...90,209-210,223 
  ssrfGuard.ts     |   77.22 |    85.36 |     100 |   77.22 | ...57,261-267,273 
  trustedHooks.ts  |       0 |        0 |       0 |       0 | 1-124             
  types.ts         |   90.18 |    90.78 |   85.18 |   90.18 | ...91-392,452-456 
  urlValidator.ts  |     100 |      100 |     100 |     100 |                   
 src/ide           |   74.28 |    83.39 |   78.33 |   74.28 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |    64.2 |    81.48 |   66.66 |    64.2 | ...9-970,999-1007 
  ide-installer.ts |   89.06 |    79.31 |     100 |   89.06 | ...36,143-147,160 
  ideContext.ts    |     100 |      100 |     100 |     100 |                   
  process-utils.ts |   84.84 |    71.79 |     100 |   84.84 | ...37,151,193-194 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/lsp           |   33.92 |    45.16 |   45.76 |   33.92 |                   
  ...nfigLoader.ts |   70.27 |    35.89 |   94.73 |   70.27 | ...20-422,426-432 
  ...ionFactory.ts |    4.29 |      100 |       0 |    4.29 | ...20-371,377-394 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   13.52 |    81.25 |   29.16 |   13.52 | ...75-694,700-730 
  ...eLspClient.ts |   17.89 |      100 |       0 |   17.89 | ...37-244,254-258 
  ...LspService.ts |   45.87 |    62.13 |   66.66 |   45.87 | ...1282,1299-1309 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |   78.69 |    75.34 |   75.92 |   78.69 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   73.82 |    53.92 |     100 |   73.82 | ...88-895,902-904 
  ...en-storage.ts |   98.62 |    97.72 |     100 |   98.62 | 87-88             
  oauth-utils.ts   |   70.58 |    85.29 |    90.9 |   70.58 | ...70-290,315-344 
  ...n-provider.ts |   89.83 |    95.83 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   79.52 |    86.66 |   86.36 |   79.52 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   82.87 |    82.35 |   92.85 |   82.87 | ...63-173,181-182 
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   68.14 |    82.35 |   64.28 |   68.14 | ...81-295,298-314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/memory        |   67.44 |       76 |   65.62 |   67.44 |                   
  const.ts         |     100 |      100 |     100 |     100 |                   
  dream.ts         |   65.65 |    73.33 |      50 |   65.65 | 50,107-148        
  ...entPlanner.ts |   57.84 |    72.72 |   33.33 |   57.84 | ...35,140-147,152 
  entries.ts       |   63.77 |    79.16 |      50 |   63.77 | ...72-180,183-189 
  extract.ts       |    95.2 |    79.16 |     100 |    95.2 | 81-86,125         
  ...entPlanner.ts |   63.08 |    65.71 |   41.17 |   63.08 | ...17,222-223,332 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |    45.8 |    61.53 |   44.44 |    45.8 | ...04,211,214-346 
  indexer.ts       |   83.87 |    45.45 |     100 |   83.87 | ...50,56-57,69-70 
  manager.ts       |   75.31 |    81.04 |    75.6 |   75.31 | ...1278,1291-1293 
  memoryAge.ts     |   90.47 |    77.77 |     100 |   90.47 | 50-51             
  paths.ts         |   55.47 |    89.47 |   85.71 |   55.47 | ...,89-90,106-114 
  prompt.ts        |   93.36 |    71.42 |     100 |   93.36 | ...58,161,228-229 
  recall.ts        |   79.56 |    69.38 |   88.88 |   79.56 | ...40-245,269-280 
  ...ceSelector.ts |   91.95 |    77.27 |     100 |   91.95 | ...08,110-111,119 
  scan.ts          |   87.91 |    68.42 |     100 |   87.91 | ...47-48,58,82-87 
  ...entPlanner.ts |    11.5 |      100 |       0 |    11.5 | ...57-192,210-298 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   94.44 |    83.33 |     100 |   94.44 | 56-57,92-93       
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   89.31 |    85.47 |    87.5 |   89.31 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   90.24 |    91.42 |     100 |   90.24 | 142,148,151-160   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |       44 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.63 |    92.53 |     100 |   98.63 | 161,323,329       
  modelRegistry.ts |     100 |    98.59 |     100 |     100 | 222               
  modelsConfig.ts  |   84.57 |    81.92 |   81.57 |   84.57 | ...1223,1252-1253 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/output        |     100 |      100 |     100 |     100 |                   
  ...-formatter.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/permissions   |   71.18 |    88.73 |   48.57 |   71.18 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   81.42 |    86.66 |      80 |   81.42 | ...29-830,837-846 
  rule-parser.ts   |   95.99 |    93.18 |     100 |   95.99 | ...-864,1013-1015 
  ...-semantics.ts |   58.28 |    85.27 |    30.2 |   58.28 | ...1604-1614,1643 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/prompts       |   83.63 |      100 |    87.5 |   83.63 |                   
  mcp-prompts.ts   |   18.18 |      100 |       0 |   18.18 | 11-19             
  ...t-registry.ts |     100 |      100 |     100 |     100 |                   
 src/qwen          |   86.01 |    79.48 |   97.18 |   86.01 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   84.99 |    74.81 |   93.33 |   84.99 | ...,985-1001,1031 
  ...kenManager.ts |   83.76 |    76.22 |     100 |   83.76 | ...62-767,788-793 
 src/services      |   86.81 |    84.92 |   90.06 |   86.81 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |   97.82 |    94.73 |     100 |   97.82 | 172-173           
  ...ionService.ts |   95.53 |    95.14 |     100 |   95.53 | ...92,354,356-360 
  ...ingService.ts |    84.1 |    84.35 |   82.85 |    84.1 | ...1240,1257-1258 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  cronScheduler.ts |   97.56 |    92.98 |     100 |   97.56 | 62-63,77,155      
  ...eryService.ts |   80.43 |    95.45 |      75 |   80.43 | ...19-134,140-141 
  fileReadCache.ts |     100 |      100 |     100 |     100 |                   
  ...temService.ts |   89.76 |     85.1 |   88.88 |   89.76 | ...89,191,266-273 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  gitService.ts    |   68.75 |     92.3 |   55.55 |   68.75 | ...12-122,125-129 
  ...reeService.ts |   71.83 |    68.47 |    91.3 |   71.83 | ...89-790,806,822 
  ...ionService.ts |   98.13 |     97.8 |   95.45 |   98.13 | ...32-333,380-381 
  ...orRegistry.ts |   96.34 |    91.66 |     100 |   96.34 | ...90-391,542-543 
  sessionRecap.ts  |   12.34 |      100 |       0 |   12.34 | 49-158            
  ...ionService.ts |   89.42 |     78.1 |   96.42 |   89.42 | ...1221,1225-1226 
  sessionTitle.ts  |   93.91 |    71.15 |     100 |   93.91 | ...34-237,268-269 
  ...ionService.ts |   83.01 |    78.66 |   87.75 |   83.01 | ...1482,1488-1493 
  ...UseSummary.ts |   94.63 |    88.67 |     100 |   94.63 | ...69-171,221-222 
 ...icrocompaction |   98.62 |    86.44 |     100 |   98.62 |                   
  microcompact.ts  |   98.62 |    86.44 |     100 |   98.62 | 138,142           
 src/skills        |    87.5 |     83.8 |   94.23 |    87.5 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |     93.1 |     100 |     100 | 93,112            
  skill-load.ts    |   92.94 |    81.63 |     100 |   92.94 | ...06,226,238-240 
  skill-manager.ts |   83.31 |    79.66 |   90.32 |   83.31 | ...1115,1122-1126 
  skill-paths.ts   |   86.74 |    77.77 |     100 |   86.74 | ...00-101,106-107 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/subagents     |   82.84 |    79.74 |   95.23 |   82.84 |                   
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...-selection.ts |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   76.74 |    71.42 |   92.85 |   76.74 | ...1155,1177-1178 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 51-56,69-74,78-83 
 src/telemetry     |    73.5 |    85.47 |   77.56 |    73.5 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...-exporters.ts |   46.37 |      100 |   44.44 |   46.37 | ...85,88-89,92-93 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...-processor.ts |   93.89 |    90.21 |   94.11 |   93.89 | ...70-275,294-295 
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |    51.9 |       64 |   57.77 |    51.9 | ...1214,1231-1251 
  metrics.ts       |    74.9 |    82.95 |   74.54 |    74.9 | ...58-978,981-992 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  sdk.ts           |   90.42 |    83.56 |   76.92 |   90.42 | ...16-317,337-341 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |   92.77 |     87.3 |     100 |   92.77 | 79-93,379-383     
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |   99.24 |    88.88 |     100 |   99.24 | 53                
  types.ts         |   79.17 |    85.83 |   83.33 |   79.17 | ...1149,1152-1181 
  uiTelemetry.ts   |   92.97 |    96.96 |   81.25 |   92.97 | ...93-194,200-207 
 ...ry/qwen-logger |   68.24 |    79.56 |   64.91 |   68.24 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   68.24 |    79.34 |   64.28 |   68.24 | ...1055,1093-1094 
 src/test-utils    |   93.16 |    95.83 |   73.52 |   93.16 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  ...st-helpers.ts |   94.11 |       90 |     100 |   94.11 | 69-70             
  index.ts         |     100 |      100 |     100 |     100 |                   
  mock-tool.ts     |   91.19 |    97.05 |   68.96 |   91.19 | ...38,202-203,216 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   76.49 |    81.35 |   84.43 |   76.49 |                   
  ...erQuestion.ts |   88.93 |    76.74 |    90.9 |   88.93 | ...39-340,347-348 
  cron-create.ts   |   97.75 |    88.88 |   83.33 |   97.75 | 30-31             
  cron-delete.ts   |   96.82 |      100 |   83.33 |   96.82 | 26-27             
  cron-list.ts     |   96.66 |      100 |   83.33 |   96.66 | 25-26             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  edit.ts          |   78.01 |    84.76 |   73.33 |   78.01 | ...86-687,774-824 
  exitPlanMode.ts  |   85.09 |    85.71 |     100 |   85.09 | ...60-163,177-189 
  glob.ts          |   90.63 |    88.33 |   84.61 |   90.63 | ...28,171,302,305 
  grep.ts          |   79.19 |    85.71 |   78.94 |   79.19 | ...20,560,569-576 
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.77 |    60.09 |   90.32 |   72.77 | ...1211,1213-1214 
  ...nt-manager.ts |   51.95 |     65.9 |   47.36 |   51.95 | ...03-525,528-565 
  mcp-client.ts    |   32.44 |    75.28 |   63.63 |   32.44 | ...1462,1466-1469 
  mcp-tool.ts      |   90.98 |    88.88 |   96.42 |   90.98 | ...95-596,646-647 
  memory-config.ts |       0 |        0 |       0 |       0 | 1-47              
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 102,109           
  monitor.ts       |   92.27 |    83.94 |      92 |   92.27 | ...18,547-550,563 
  ...nforcement.ts |   82.44 |       90 |     100 |   82.44 | 174-185,234-247   
  read-file.ts     |   95.07 |     88.6 |      90 |   95.07 | ...99,290-293,296 
  ripGrep.ts       |   94.59 |    85.71 |   93.33 |   94.59 | ...60,463,541-542 
  ...-transport.ts |    6.34 |        0 |       0 |    6.34 | 47-145            
  send-message.ts  |   89.32 |    91.66 |   83.33 |   89.32 | 44-45,68-76       
  shell.ts         |   72.18 |    80.23 |   89.65 |   72.18 | ...3659,3708-3714 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   88.11 |    91.17 |   84.61 |   88.11 | ...95,399,422-444 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  todoWrite.ts     |   85.42 |    84.09 |   84.61 |   85.42 | ...05-410,432-433 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   74.64 |    75.24 |      80 |   74.64 | ...82-783,791-792 
  tool-search.ts   |   95.19 |    86.48 |    92.3 |   95.19 | ...47-153,208-213 
  tools.ts         |   87.76 |       90 |   88.23 |   87.76 | ...50-451,467-473 
  web-fetch.ts     |   88.59 |    79.48 |    92.3 |   88.59 | ...12-313,315-316 
  write-file.ts    |    79.2 |    79.26 |   83.33 |    79.2 | ...39-642,654-689 
 src/tools/agent   |   83.39 |    84.95 |   83.92 |   83.39 |                   
  agent.ts         |   83.69 |    85.38 |   84.31 |   83.69 | ...1668,1677-1681 
  fork-subagent.ts |   78.26 |    71.42 |      80 |   78.26 | 54-72,104-105     
 src/utils         |   88.63 |    87.14 |   93.16 |   88.63 |                   
  LruCache.ts      |       0 |        0 |       0 |       0 | 1-41              
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   76.08 |    44.44 |     100 |   76.08 | 61-70,72          
  bareMode.ts      |   27.27 |      100 |       0 |   27.27 | 9-15,18-19        
  browser.ts       |    7.69 |      100 |       0 |    7.69 | 17-56             
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   89.11 |    86.66 |     100 |   89.11 | ...28-129,132-133 
  cronDisplay.ts   |   42.85 |    23.07 |     100 |   42.85 | 26-31,33-45,47-54 
  cronParser.ts    |   89.74 |    85.71 |     100 |   89.74 | ...,63-64,183-186 
  debugLogger.ts   |    95.9 |    93.84 |   94.73 |    95.9 | 106-107,214-218   
  editHelper.ts    |   93.63 |    83.52 |     100 |   93.63 | ...28-429,463-464 
  editor.ts        |   97.61 |    95.71 |     100 |   97.61 | ...70-271,273-274 
  ...arResolver.ts |   94.28 |    88.88 |     100 |   94.28 | 28-29,125-126     
  ...entContext.ts |     100 |    95.45 |     100 |     100 | 83                
  errorParsing.ts  |    97.7 |    97.05 |     100 |    97.7 | 72-73             
  ...rReporting.ts |   88.46 |       90 |     100 |   88.46 | 69-74             
  errors.ts        |   70.92 |    79.59 |   53.33 |   70.92 | ...03-219,223-229 
  fetch.ts         |   70.18 |    71.42 |   71.42 |   70.18 | ...42,148,161,186 
  fileUtils.ts     |   91.41 |    86.07 |      95 |   91.41 | ...1182,1186-1192 
  forkedAgent.ts   |    78.5 |    70.73 |   85.71 |    78.5 | ...30-436,441-447 
  formatters.ts    |   54.54 |       50 |     100 |   54.54 | 12-16             
  ...eUtilities.ts |   89.21 |    86.66 |     100 |   89.21 | 16-17,49-55,65-66 
  ...rStructure.ts |   94.36 |    94.28 |     100 |   94.36 | ...17-120,330-335 
  getPty.ts        |    12.5 |      100 |       0 |    12.5 | 21-34             
  gitDiff.ts       |   92.36 |    79.53 |     100 |   92.36 | ...55-856,928-929 
  ...noreParser.ts |    92.3 |    89.36 |     100 |    92.3 | ...15-116,186-187 
  gitUtils.ts      |   56.66 |    85.71 |      75 |   56.66 | ...2,72-73,97-148 
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 26                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |    74.1 |    90.76 |   58.33 |    74.1 | ...23-326,336-342 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...yDiscovery.ts |    83.9 |    79.36 |     100 |    83.9 | ...16,319,411-414 
  ...tProcessor.ts |   93.63 |       90 |     100 |   93.63 | ...96-302,384-385 
  ...Inspectors.ts |   61.53 |      100 |      50 |   61.53 | 18-23             
  ...kerChecker.ts |   82.55 |    78.57 |     100 |   82.55 | 68-69,79-84,92-98 
  notebook.ts      |   94.35 |    84.78 |     100 |   94.35 | ...10,122,174-176 
  openaiLogger.ts  |   87.93 |    82.85 |     100 |   87.93 | ...22-124,147-152 
  partUtils.ts     |     100 |      100 |     100 |     100 |                   
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   93.21 |    91.86 |     100 |   93.21 | ...89-390,392-394 
  pdf.ts           |   93.68 |    87.05 |     100 |   93.68 | ...96-297,321-325 
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  ...ectSummary.ts |   89.39 |    72.41 |     100 |   89.39 | ...37-142,193-196 
  ...tIdContext.ts |     100 |      100 |     100 |     100 |                   
  proxyUtils.ts    |     100 |      100 |     100 |     100 |                   
  ...rDetection.ts |   58.57 |       76 |     100 |   58.57 | ...4,88-89,95-100 
  ...noreParser.ts |   85.45 |    85.18 |     100 |   85.45 | ...59,65-66,72-73 
  rateLimit.ts     |   92.55 |    85.92 |     100 |   92.55 | ...70-272,309-310 
  readManyFiles.ts |   87.96 |    86.95 |     100 |   87.96 | ...05-207,223-234 
  retry.ts         |   89.81 |    88.05 |     100 |   89.81 | ...29,350,357-358 
  ripgrepUtils.ts  |   46.53 |    84.37 |   66.66 |   46.53 | ...32-233,245-322 
  ...sDiscovery.ts |   97.42 |    92.85 |     100 |   97.42 | ...04,182-183,202 
  ...tchOptions.ts |   63.85 |    64.28 |   83.33 |   63.85 | ...29-130,187-188 
  runtimeStatus.ts |   85.58 |    82.05 |     100 |   85.58 | ...81,231-237,239 
  safeJsonParse.ts |   74.07 |    83.33 |     100 |   74.07 | 40-46             
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   90.78 |    88.23 |     100 |   90.78 | ...41-42,93,95-96 
  ...aValidator.ts |   94.57 |    80.26 |     100 |   94.57 | ...04,213-216,270 
  ...r-launcher.ts |   76.92 |     91.3 |   66.66 |   76.92 | ...34,136,157-195 
  ...orageUtils.ts |   96.89 |    85.84 |     100 |   96.89 | ...51,367,447,466 
  shell-utils.ts   |   82.93 |    89.55 |     100 |   82.93 | ...1522,1529-1533 
  ...lAstParser.ts |   95.58 |    85.79 |     100 |   95.58 | ...1059-1061,1071 
  ...nlyChecker.ts |   95.75 |    92.39 |     100 |   95.75 | ...00-301,313-314 
  sideQuery.ts     |   98.71 |    97.14 |     100 |   98.71 | 106               
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   77.77 |       50 |     100 |   77.77 | 44,54-59          
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  terminalSafe.ts  |     100 |      100 |     100 |     100 |                   
  ...Serializer.ts |   98.72 |       90 |     100 |   98.72 | 42-43,134,201-203 
  testUtils.ts     |   53.33 |      100 |   33.33 |   53.33 | ...53,59-64,70-72 
  textUtils.ts     |      60 |      100 |   66.66 |      60 | 36-55             
  thoughtUtils.ts  |     100 |    92.85 |     100 |     100 | 71                
  ...-converter.ts |   94.59 |    85.71 |     100 |   94.59 | 35-36             
  tool-utils.ts    |    93.6 |     91.3 |     100 |    93.6 | ...58-159,162-163 
  truncation.ts    |     100 |       92 |     100 |     100 | 52,71             
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   93.71 |    89.28 |   93.33 |   93.71 | ...24-225,249-251 
  xml.ts           |     100 |      100 |     100 |     100 |                   
  yaml-parser.ts   |      92 |    84.31 |     100 |      92 | 49-53,65-69       
 ...ils/filesearch |   85.77 |    81.06 |   96.42 |   85.77 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |   82.84 |    77.49 |   94.82 |   82.84 | ...1451,1485-1486 
  fileSearch.ts    |   93.58 |    87.32 |     100 |   93.58 | ...46-247,249-250 
  ignore.ts        |     100 |      100 |     100 |     100 |                   
  result-cache.ts  |     100 |     92.3 |     100 |     100 | 46                
 ...uest-tokenizer |   56.63 |    74.52 |   74.19 |   56.63 |                   
  ...eTokenizer.ts |   41.86 |    76.47 |   69.23 |   41.86 | ...70-443,453-507 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |      76 |      100 |   33.33 |      76 | 45-48,55-56       
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
-------------------|---------|----------|---------|---------|-------------------

For detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Additional suggestions (no specific diff line):

  • [Suggestion] Missing reasoning delta normalization test — 3 new tests cover text content only. The reasoning path (converter.ts:1087, same normalizeStreamingTextDelta call with reasoningDeltaState) has zero test coverage. Add a test sending cumulative reasoning_content deltas and asserting suffix deduplication.
  • [Suggestion] Missing cumulative mode exit test — No test covers the cumulativeMode = false transition path (converter.ts:93). Add a test: establish cumulative mode, then send a non-matching chunk, verify state exits correctly.

if (state.cumulativeMode) {
if (rawDelta.startsWith(state.emittedText)) {
const suffix = rawDelta.slice(state.emittedText.length);
state.emittedText = rawDelta;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Cumulative mode causes O(n²) performance: each chunk performs rawDelta.startsWith(state.emittedText) (O(n) scan over accumulated text) and state.emittedText = rawDelta (full string copy). For a 50KB response via ~500 chunks, that's ~25M character operations on the hot streaming path.

Suggested change
state.emittedText = rawDelta;
// Use a length counter instead of full-string copy in cumulative mode
state.emittedLength = rawDelta.length;

Alternatively, track emittedLength as a number counter and use rawDelta.slice(state.emittedLength) for suffix extraction, avoiding O(n) startsWith and copy on every chunk.

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — the cumulative branch is O(prev) per chunk and O(n²) total. Concrete numbers:

  • 50 KB / 500 chunks ≈ 12.5 M char-ops on the hot path
  • 1 MB / 5 K chunks ≈ 1.25 G char-ops — measurable

I'll defer the optimization to a follow-up PR rather than carry it in this minimal split, because the safe-shape variant is non-trivial:

  • Pure emittedLength tracking drops the startsWith safety check. If an upstream glitches and sends a chunk that no longer prefix-extends, we silently emit garbage from the wrong slice offset. The current startsWith is what triggers the cumulative-mode exit (line 91 in this PR).
  • A correct refactor needs either (a) a periodic verify (e.g. every Nth chunk), or (b) a length-only fast path with a divergence-detection trap (chunk shorter than expected → fall back).

Given current real-world stream sizes (most observed cumulative-delta upstreams produce <100 KB before finish), the perf is below the noise floor of provider RTT. I'd rather land this as is and follow up with the perf refactor + a benchmark covering the 1 MB case.

Tracking as TODO; will not be a blocker for the cumulative-bug-fix landing.

}

if (
rawDelta.startsWith(state.emittedText) &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Step 4 (prefix-based cumulative mode entry) lacks a minimum length threshold, unlike Step 5 which uses CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH = 20. A non-cumulative provider that emits "foo" then "foobar" would trigger cumulative mode with just 3-char overlap — increasing false-positive risk.

Suggested change
rawDelta.startsWith(state.emittedText) &&
if (
rawDelta.startsWith(state.emittedText) &&
rawDelta.length > state.emittedText.length &&
state.emittedText.length >= CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH
) {

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I considered this and want to push back: a min-length threshold on prefix-based entry would delay legitimate cumulative-mode detection on streams where the early accumulated text is small.

Concrete failing example:

Chunk 1: "Hi"                  (2 b)
Chunk 2: "Hi there"            (8 b)  — cumulative
Chunk 3: "Hi there friend"     (15 b) — cumulative
Chunk 4: "Hi there friend, ..."     — finally crosses 20 b threshold

With the suggested threshold, chunks 2 and 3 fall through to the non-cumulative append path and we emit "Hi" + "Hi there" + "Hi there friend" → user sees "HiHi thereHi there friend" before chunk 4 finally enters cumulative mode. By then the corrupted prefix is already on screen.

The false-positive case the suggestion guards against ("foo" followed by "foobar" from a non-cumulative provider) requires the second chunk to literally include the previous chunk's full content as a prefix. Real incremental providers don't do this — they emit only the new tokens. The pattern only appears on actually-cumulative providers, which is what we want to detect.

Net: keeping the current aggressive entry. The exact-repeat path (line 106) is the one that warrants a length floor — and that one already has it.


if (
rawDelta === state.emittedText &&
rawDelta.length >= CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Step 5 exact-repeat heuristic (rawDelta === emittedText && length >= 20) can false-positive on legitimate repeated content from non-cumulative providers. If a model genuinely repeats a ≥20-char chunk verbatim (e.g., a repeated paragraph), it gets silently dropped. Consider requiring two consecutive identical chunks before locking in cumulative mode:

Suggested change
rawDelta.length >= CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH
state.repeatCount = (state.repeatCount ?? 0) + 1;
if (state.repeatCount >= 2) {
state.cumulativeMode = true;
}
return '';
}
state.repeatCount = 0;

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Considered. The trade-off:

Behaviour Current (1× lock-in) Suggestion (2× lock-in)
Cumulative provider exact-repeats accumulated text silent on 2nd → correct emits 2× → first dup leaks
Model legitimately repeats a ≥20-char chunk verbatim silent on 2nd → minor loss preserves 2nd; silent from 3rd

The leftover failure mode of the suggested variant: cumulative providers do still leak one full duplicate of the response prefix into the live preview before the lock-in fires. That defeats the demo evidence where current behaviour drops the amplification ratio cleanly to 1.0× starting at chunk 2.

Counter-evidence for the false-positive concern: in practice token streaming chunks the model output, so a literal exact-repeat of a ≥20-char chunk requires the model to repeat AND the tokenizer to align the boundaries identically — extremely rare. Per-stream observability (added in commit 136017fdebugLogger.debug traces at every cumulative-mode transition) gives us a way to detect if this ever fires falsely in production.

Defer for now; revisit if observability traces show false-positive entries.

// Some OpenAI-compatible providers send accumulated content in each
// delta.content field. Normalize that shape to incremental suffixes before the
// Gemini stream layer appends it to the live transcript.
function normalizeStreamingTextDelta(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] normalizeStreamingTextDelta has zero logging or observability. When cumulative mode activates, deactivates, or chunks are silently suppressed (returning ""), there is no trace. This makes diagnosing output truncation/duplication bugs extremely difficult. Use the existing debugLogger to emit trace-level events at mode transitions.

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Adopted in commit 136017f. Three new debugLogger.debug trace points on the existing CONVERTER channel:

  • entry via prefix-overlap (prev=Nb -> curr=Mb)
  • entry via exact-repeat (Nb)
  • exit on prefix mismatch in cumulative mode

No perf impact when the channel is disabled (the default in production).


// Handle text content
if (typeof choice.delta?.content === 'string') {
const normalizedContent = normalizeStreamingTextDelta(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Normalization is applied unconditionally to all providers via convertOpenAIChunkToGemini. The heuristic is probabilistic — any non-cumulative provider whose chunk coincidentally starts with the previous full text gets misclassified. The codebase already has per-provider feature flags (e.g., splitToolMedia). Consider adding an opt-out mechanism (e.g., normalizeCumulativeDeltas?: boolean in ContentGeneratorConfig, defaulting to true for backward compatibility) for known-incremental providers.

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Defer. Reasoning:

  • Auto-detection has worked for every cumulative-delta provider seen in the issue queue (总是显示一下错误 #3279 / 回答时经常出现频闪的情况 #1184 / 终端界面无限滚动/刷新循环 #3838 / 0.15.6 界面输出内容过程中闪烁,之前是展开过多才闪烁 #3806 — all DashScope variants). No cross-provider misclassification has been reported.
  • The detection itself is conservative — exact-repeat needs ≥20 chars, prefix-overlap needs startsWith to actually match. Random non-cumulative providers do not produce these patterns by accident (real incremental SSE streams emit only token suffixes, never the prefix).
  • Adding a ContentGeneratorConfig.normalizeCumulativeDeltas flag now would be premature: we don't have a known provider that needs to opt out.
  • The newly added debugLogger.debug traces (commit 136017f) give us the diagnostic tool to detect a misclassification in the wild without a config knob — if a user reports content loss on a non-cumulative upstream, we'd see the entry trace and know to add an opt-out.

Will revisit if a real opt-out case appears.

…alization

Address review on #3896:

- Reasoning path coverage: cumulative `delta.reasoning_content` chunks
  now have an explicit test that mirrors the text-content cumulative
  test, asserting the same suffix-extraction behaviour and that emitted
  parts carry `thought: true`.

- Cumulative-mode exit path: previously untested branch (converter.ts:
  `state.cumulativeMode = false` on prefix mismatch). New test
  establishes cumulative mode with two prefix-extending chunks, then
  sends a non-matching chunk; asserts the chunk is appended verbatim
  (no silent loss).

- Add `debugLogger.debug` traces at each cumulative-mode transition
  (entry via prefix overlap, entry via exact-repeat, exit on prefix
  mismatch). Opt-in via existing CONVERTER debug channel — no perf
  impact when disabled.

Generated with AI

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

chiga0 commented May 7, 2026

Copy link
Copy Markdown
Collaborator Author

@wenshao thanks for the thorough review. Pushed 136017f78 addressing the test gaps + observability; replied inline to each of the line-level points. Summary:

Adopted in 136017f78:

Point Status Where
Missing reasoning delta normalization test ✅ added should normalize cumulative streaming reasoning_content deltas to suffixes
Missing cumulative mode exit test ✅ added should exit cumulative mode when a chunk does not match prior accumulated text
Observability via debugLogger ✅ added 3 trace points: prefix-overlap entry, exact-repeat entry, prefix-mismatch exit

Tests: 85 / 85 (was 83) — no regressions in the converter test file.

Pushed back / deferred (with rationale in inline replies):

Point Status Reason
O(n²) cumulative-mode performance Defer to follow-up PR A correct length-only fast path needs a divergence-detection trap; not a single-line change. Below noise floor of provider RTT for current real-world stream sizes (<100 KB observed). Will land with a benchmark covering the 1 MB pathological case.
Min-length threshold on prefix-based entry Decline Would delay legitimate cumulative-mode detection on streams with small early accumulated text — corrupted prefix would render before threshold fires. Real incremental providers do not emit prefix-extending chunks.
Two-consecutive-repeat lock-in Defer Would leak one full duplicate of the response prefix before lock-in, defeating the demo evidence (1.0× amplification from chunk 2 onward). False-positive risk is now diagnosable via the new debugLogger traces.
Per-provider opt-out flag Defer Auto-detection has worked for every reported case. Adding the flag now is premature; the new traces give us the diagnostic surface to detect a misclassified provider without a config knob.

Happy to revisit any of the deferrals — the inline threads have the full reasoning chain.

debugLogger.debug(
'normalizeStreamingTextDelta: exiting cumulative mode (chunk does not match prior accumulated text)',
);
state.cumulativeMode = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] 退出累积模式后 emittedText 未被重置,导致后续状态污染。

cumulativeMode = false 后,代码穿透到非累积逻辑最终执行 state.emittedText += rawDelta(第 121 行),将新 chunk 拼接到旧的累积文本后。如果同一流中 provider 后续恢复发送累积式 chunk,startsWith 前缀检查会因 emittedText 已损坏而失败,导致累积内容原样输出(重复)。

Suggested change
state.cumulativeMode = false;
debugLogger.debug(
'normalizeStreamingTextDelta: exiting cumulative mode (chunk does not match prior accumulated text)',
);
state.cumulativeMode = false;
state.emittedText = rawDelta;
return rawDelta;

现有测试 should exit cumulative mode 仅验证输出正确,未检查 emittedText 内部状态。建议增加后续累积恢复的断言。

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in commit b2da918. When exiting cumulative mode the function now sets state.emittedText = rawDelta and returns immediately, so the new chunk becomes the fresh baseline for subsequent prefix checks instead of being appended to the stale accumulated text.

reasoning?: string | null;
}

const CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH = 20;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH = 20 阈值缺乏选值理由的文档说明。

为什么是 20 而不是 10 或 50?该阈值决定了精确重复 chunk 是被静默吞入(≥20)还是穿透输出(<20),但没有任何注释解释选值依据。采用不同 tokenization 策略的 provider 可能需要不同阈值。

Suggested change
const CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH = 20;
// 累积式 provider 通常按 token(≥2 字符)而非单个字符发送增量,
// 因此 < 20 字符的精确重复大概率是合法增量内容(如 "ha" + "ha")。
const CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH = 20;

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added in commit fc2492c. The comment now explains: cumulative providers typically emit whole words/phrases (≥20 chars), so sub-word repeats like "ha" are more likely to be valid incremental content.

}

// Handle text content
if (typeof choice.delta?.content === 'string') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] contentreasoning 的空结果处理不对称。

reasoning 文字在第 1103 行有 if (normalizedReasoningText) 守卫来跳过空结果,但 content 文字(第 1110-1117 行)直接将可能为空字符串的 normalizedContent 传给 convertOpenAITextToParts,缺少同样的守卫。当 provider 发送被标准化为空的 content delta 时,可能产生空 text part。

Suggested change
if (typeof choice.delta?.content === 'string') {
if (typeof choice.delta?.content === 'string') {
const normalizedContent = normalizeStreamingTextDelta(
choice.delta.content,
(requestContext.textDeltaState ??= {
emittedText: '',
cumulativeMode: false,
}),
);
if (normalizedContent) {
parts.push(
...convertOpenAITextToParts(
normalizedContent,

或在确认 convertOpenAITextToParts 能正确处理空字符串后添加注释说明。

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in commit b2da918. The content path now has a if (normalizedContent || choice.finish_reason) guard matching the reasoning path, so empty-string deltas mid-stream no longer produce empty text parts.

@@ -26,6 +31,8 @@ export interface RequestContext {
// user message for strict OpenAI-compat servers. See ContentGeneratorConfig
// for details.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] textDeltaStatereasoningDeltaState 的生命周期通过 ??= 隐式绑定于 RequestContext,但未在类型定义或 createRequestContext 工厂中显式记录。

若未来有人引入 RequestContext 池化复用(它携带 modelmodalities 等看似可复用的字段),残留的增量状态会静默破坏后续请求的文本标准化。

建议在 createRequestContext 中显式初始化 textDeltaState: undefined, reasoningDeltaState: undefined,并在接口 JSDoc 中注明这些字段是「流式请求作用域内的可变状态,不可跨请求复用」。

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added JSDoc to both fields in commit fc2492c. The comments now explicitly say: per-stream mutable state, lazily initialised, must not be shared or reused across requests — stale state will silently corrupt text output. The reasoning channel also notes that the two channels are tracked independently for correct interleaved-chunk deduplication.

if (
rawDelta.startsWith(state.emittedText) &&
rawDelta.length > state.emittedText.length
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The prefix-overlap heuristic can corrupt valid incremental streams. A normal OpenAI-style provider may emit chunks like "Here" followed by "Here is..."; those should append to "HereHere is...", but this code treats the second chunk as cumulative and emits only " is...". Similarly, long exact repeats are dropped entirely. That changes model output for standards-compliant incremental providers.

A safer fix is to avoid enabling cumulative normalization purely from text-prefix coincidences. Gate this behavior behind provider/model capability/configuration, or use a detector that does not suppress ambiguous prefix-extension/exact-repeat chunks by default.

— gpt-5.5 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No code change here — this is by design.

A standard OpenAI-protocol incremental provider sends only the new tokens as each delta: chunk N is just the fresh text, never a prefix of chunk N+1. The only way `rawDelta.startsWith(state.emittedText)` can be true is when the provider sends the full accumulated transcript in each chunk (DashScope / some vLLM-compatible deployments). A well-behaved incremental provider never does this — the two content shapes are mutually exclusive.

The scenario in the concern — chunk A = "Here", chunk B = "Here is..." — would require chunk B to include chunk A verbatim as a prefix, which is exactly what cumulative providers do and exactly what incremental providers never do. If a provider did emit chunk B = "Here is..." incrementally, state.emittedText at that point would already contain "Here" + any earlier tokens (e.g. 100+ chars), so rawDelta.startsWith(state.emittedText) would be false.

Adding a config gate for this heuristic would make it harder to use without providing safety for a scenario that cannot occur in practice with compliant providers.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I re-checked this one and I think the concern still stands. The issue is not that an incremental provider sends the previous transcript; it is that the new incremental chunk text itself can legitimately start with the previous chunk text. With the current heuristic, a valid stream like ["Here", "Here is intentionally repeated."] should render HereHere is intentionally repeated., but it normalizes to Here is intentionally repeated. because the second chunk is treated as cumulative and only the suffix is emitted. The same applies to a legitimate long exact-repeat chunk, which would be dropped entirely after the first occurrence.

So I would not treat this as impossible for compliant incremental providers. Either this normalization needs to be gated to providers/models known to emit cumulative deltas, or the detector needs to avoid suppressing ambiguous prefix-extension/exact-repeat chunks by default.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks — re-examining your scenario, you'''re right that the literal pattern is reproducible by a non-cumulative provider that legitimately repeats prior text inside the new chunk. I want to enumerate the trade-off more carefully before deciding whether to gate.

The detector is conservative on entry but not on continuation.

Entry paths:

  1. Prefix-overlap (line 109): rawDelta.startsWith(emittedText) && rawDelta.length > emittedText.length — your example hits this.
  2. Exact-repeat (line 100): same chunk twice, both ≥20 chars — much harder to hit accidentally.

For entry path #1 to fire on incremental output, the provider must emit a chunk whose payload starts with the entire prior accumulated transcript verbatim. Concrete shape: chunk N = X, chunk N+1 = X + Y where X is the full prior accumulated text including all chunks before N, not just chunk N alone. As the stream grows, this becomes increasingly unlikely for a streaming-incremental provider — a 200-char chunk would need to literally include the full 200-char prior transcript as its prefix.

Mitigations already in place against false-positive entries:

  • 1024-byte detection window cap (commit fc2492c / hardened in d132acb): once 1024 bytes of incremental text have accumulated, the baseline freezes and the prefix check stops running. Late-arriving chunks pass through verbatim.
  • Cumulative-mode exit (line 91): if a chunk in cumulative mode does not prefix-extend the accumulated text, the function exits cumulative mode and treats the new chunk as the fresh baseline (commit b2da918). So even if entry misfires once, it self-corrects on the next non-matching chunk.
  • debugLogger.debug traces on every entry, exit, and suppression path (commit 136017f + d132acb): a misclassification is observable in the wild without re-running with extra instrumentation.

Why I'''d still defer the config gate:

That said, your point that this is not impossible for compliant incremental providers stands, and I want to record it openly. If you'''d like, I can:

(a) tighten entry path #1 further by requiring the prior baseline to be ≥ some min length (e.g. 50 chars) so trivial prefix-coincidences inside small early chunks don'''t fire it; or
(b) add the explicit per-provider opt-out flag now, even unused.

I'''d slightly prefer (a) over (b) because it preserves auto-detection without a config matrix. Happy to make the call you prefer.

debugLogger.debug(
`normalizeStreamingTextDelta: entered cumulative mode (prefix overlap, prev=${state.emittedText.length - suffix.length}b -> curr=${rawDelta.length}b)`,
);
return suffix;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Short repeated cumulative chunks poison future detection. For a valid cumulative stream like ["Hello", "Hello", "Hello world"], the second short repeat falls through to state.emittedText += rawDelta, making the baseline "HelloHello". The next cumulative extension no longer starts with that baseline, so it is appended verbatim and the user sees duplicated output such as "HelloHelloHello world".

Do not append exact repeats to emittedText just because they are short. Keep a separate last-raw/possible-cumulative state, or avoid mutating the accumulated baseline until cumulative mode is unambiguous.

— gpt-5.5 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in commit b2da918. Short exact-repeats (< 20 bytes) now return the chunk directly without touching emittedText, so the baseline stays valid for future prefix checks. Appending was the root of the "HiHi" poisoning bug. A new test covers the exact sequence from the report.

);
return '';
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Normal incremental streams now retain and scan the entire emitted transcript. Every ordinary chunk that is not classified as cumulative reaches state.emittedText += rawDelta, and future chunks run prefix checks against that ever-growing string. For long streams this adds avoidable O(n²)-style CPU work and keeps an extra full copy of the transcript in memory.

Consider tracking only bounded recent state until cumulative mode is detected, capping growth, or disabling cumulative detection once the stream is clearly incremental.

— gpt-5.5 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in commit fc2492c. Added CUMULATIVE_DETECTION_WINDOW_BYTES = 1024: once 1024 bytes have been emitted without entering cumulative mode, emittedText stops growing. The prefix-overlap and exact-repeat checks still run against the capped baseline (which is enough for detection), but string concatenation at the fallthrough path is skipped. This bounds per-stream memory to ~1KB for standard incremental providers.

parts.push({ text: reasoningText, thought: true });
const normalizedReasoningText = normalizeStreamingTextDelta(
reasoningText,
(requestContext.reasoningDeltaState ??= {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Please add a regression test for interleaving reasoning and visible content in the same stream context. The implementation relies on separate reasoningDeltaState and textDeltaState, but the new tests exercise cumulative reasoning and cumulative content only in isolation. A future refactor that accidentally shares state could still pass while truncating one channel when chunks alternate.

A focused test could send one RequestContext through alternating chunks such as reasoning_content: "Let me think", content: "Here", reasoning_content: "Let me think more", and content: "Here is the answer", then assert both channels emit independent suffixes.

— gpt-5.5 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added in commit b2da918: "should deduplicate interleaved reasoning_content and content channels independently". The test sends alternating reasoning/content chunks through the same RequestContext and asserts both channels emit independent suffixes correctly.

秦奇 and others added 2 commits May 8, 2026 13:21
Three fixes from wenshao's CHANGES_REQUESTED reviews:

1. Reset emittedText when exiting cumulative mode (line 94 critical, 2026-05-07T16:23)
   — previously the stale prefix was left in place, so a provider that
     later re-entered cumulative mode would fail the startsWith check and
     emit duplicated content.

2. Short exact-repeat passthrough without baseline mutation (line 107 critical, 2026-05-07T22:41)
   — when rawDelta === emittedText and length < 20, the old code appended
     to emittedText (e.g. "Hi" → "HiHi"), poisoning future prefix checks.
     Now passes through unchanged so the baseline stays valid for the next
     prefix-overlap detection.

3. Empty-content guard on the content normalisation call (line 1110 suggestion, 16:23)
   — add `if (normalizedContent || choice.finish_reason)` so mid-stream
     chunks that normalise to "" do not push an empty text part. Still
     calls convertOpenAITextToParts on finish_reason to flush buffered
     tagged-thinking content.

Test additions (89 → 89, 4 new cases):
- resumption after cumulative-mode exit (verifies emittedText reset)
- short-repeat baseline preservation (verifies passthrough without poison)
- reasoning_content cumulative normalisation path
- interleaved reasoning_content + content channels (independent state objects)

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add CUMULATIVE_DETECTION_WINDOW_BYTES=1024: stop growing emittedText once
  1024 bytes emitted without entering cumulative mode, bounding per-stream
  memory for standard incremental providers.
- Add comment explaining CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH=20 choice.
- Add JSDoc to textDeltaState/reasoningDeltaState in RequestContext warning
  that these are per-stream-scoped mutable state and must not be reused.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0 chiga0 requested a review from wenshao May 8, 2026 07:23
state.emittedText = rawDelta;
return rawDelta;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The emittedText.startsWith(rawDelta) rewind branch returns '' with no debugLogger.debug trace.

This is the only one of the three content-suppression paths that lacks a trace. If a provider sends a chunk that is a strict prefix of the accumulated text (network retry, proxy retransmit), content disappears silently with zero observability — the hardest failure mode to debug in production.

Suggested change
if (state.emittedText.startsWith(rawDelta)) {
debugLogger.debug(
`normalizeStreamingTextDelta: cumulative rewind suppression (emitted=${state.emittedText.length}b, chunk=${rawDelta.length}b)`,
);
return '';
}

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Adopted in commit d132acb. The cumulative-rewind suppression branch now emits a debugLogger.debug trace mirroring the other two suppression paths, so all three silent-suppression sites have observability parity:

  • entry via prefix-overlap (existing)
  • entry via exact-repeat (existing)
  • rewind suppression (this fix)cumulative rewind suppression (emitted=Nb, chunk=Mb)
  • exit on prefix mismatch (existing)

Now any production report of "content went missing on a particular chunk" can be traced through the CONVERTER debug channel without re-running with extra instrumentation.

state.emittedText = rawDelta;
return rawDelta;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] After emittedText is frozen at the CUMULATIVE_DETECTION_WINDOW_BYTES (1024) cap, the non-cumulative prefix-overlap check (rawDelta.startsWith(state.emittedText)) and exact-repeat check (rawDelta === state.emittedText) continue to run against the stale baseline. If a future chunk coincidentally starts with the frozen 1024-char prefix, the function would incorrectly enter cumulative mode and produce a content-duplicating suffix.

Consider adding an early return when the cap is reached and cumulative mode is not active:

Suggested change
if (
!state.cumulativeMode &&
state.emittedText.length >= CUMULATIVE_DETECTION_WINDOW_BYTES
) {
return rawDelta;
}
if (
rawDelta.startsWith(state.emittedText) &&
rawDelta.length > state.emittedText.length
) {

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Adopted in commit d132acb. Once emittedText.length >= CUMULATIVE_DETECTION_WINDOW_BYTES (1024) and cumulative mode has not been entered, normalizeStreamingTextDelta now early-returns the chunk verbatim before the prefix-overlap and exact-repeat checks run. The frozen baseline can no longer be matched against late-arriving chunks, eliminating the false-positive entry path you described.

New regression test added: should not enter cumulative mode after the detection window cap is reached on a non-cumulative stream — sends 100×20-char incremental chunks (2000 bytes, well past the 1024 cap) and asserts every chunk passes through verbatim.

const parts = chunk.candidates?.[0]?.content?.parts;
expect(parts).toEqual([]);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Missing boundary-value tests for CUMULATIVE_DETECTION_WINDOW_BYTES (1024) and CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH (20).

(1) No test exercises the 1024-char cap. After 1024 chars of incremental output, emittedText stops growing. A test sending >1024 chars of non-cumulative chunks should verify the cap holds.

(2) No test exercises the exact 20-char threshold. Tests use 2-char ("ha") and 49-char repeats but not 19/20 chars. A boundary test would catch an off-by-one in the >= comparison.

Suggested change
it('should enter cumulative mode at exactly the 20-char repeat threshold', () => {
const ctx = withStreamParser();
const twenty = '12345678901234567890'; // exactly 20 chars
const emitted = [twenty, twenty].map((content, index) => {
const chunk = converter.convertOpenAIChunkToGemini(
{
object: 'chat.completion.chunk',
id: `chunk-boundary-20-${index}`,
created: 456 + index,
choices: [{
index: 0,
delta: { content },
finish_reason: null,
logprobs: null,
}],
model: 'gpt-test',
} as unknown as OpenAI.Chat.ChatCompletionChunk,
ctx,
);
return chunk.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
});
expect(emitted).toEqual([twenty, '']);
});
it('should passthrough repeats below the 20-char threshold', () => {
const ctx = withStreamParser();
const nineteen = '1234567890123456789'; // 19 chars
const emitted = [nineteen, nineteen].map((content, index) => {
const chunk = converter.convertOpenAIChunkToGemini(
{
object: 'chat.completion.chunk',
id: `chunk-boundary-19-${index}`,
created: 456 + index,
choices: [{
index: 0,
delta: { content },
finish_reason: null,
logprobs: null,
}],
model: 'gpt-test',
} as unknown as OpenAI.Chat.ChatCompletionChunk,
ctx,
);
return chunk.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
});
expect(emitted).toEqual([nineteen, nineteen]);
});

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Partially adopted in commit d132acb.

Cap test (1024 bytes): added should not enter cumulative mode after the detection window cap is reached on a non-cumulative stream — sends 100 × 20-char incremental chunks (2000 bytes total, well past the cap) and asserts every chunk emits verbatim. This catches both an off-by-one in the cap comparison and any future regression that lets cumulative detection fire after the freeze.

Boundary tests (19/20 chars): these already exist (added during the previous review round, commit fc2492c):

  • should enter cumulative mode on exact 20-char repeat (at threshold) — 20-char × 2 → second emits ``, then prefix-extension emits suffix only
  • should pass through 19-char exact repeat without entering cumulative mode (below threshold) — 19-char × 2 passes both verbatim, then a prefix-extension at 25+ chars triggers cumulative entry

The 19/20 pair already provides off-by-one protection on >= versus >. Marking this thread as resolved unless you want a different framing.

const parts = chunk.candidates?.[0]?.content?.parts;
expect(parts).toEqual([]);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] No test covers the cumulative-mode rewind branch (emittedText.startsWith(rawDelta) where rawDelta is a strict prefix, shorter than emittedText). The existing "ignore repeated cumulative chunks" test covers only an exact-length repeat. A rewind scenario (e.g., provider re-sends a shorter accumulated chunk after a longer one) is untested:

Chunk 1: "Hello"          → emits "Hello", emittedText = "Hello"
Chunk 2: "Hello World"    → cumulative mode, emits " World"
Chunk 3: "Hello"          → rewind branch (strict prefix), emits ""
Chunk 4: "Hello World!"   → suffix extraction, emits "!"

This branch is the same one that lacks a debug trace (see the Critical finding on converter.ts:88). A test here would serve double duty as both coverage and a regression guard.

— glm-5.1 via Qwen Code /review

const parts = chunk.candidates?.[0]?.content?.parts;
expect(parts).toEqual([]);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Consider adding a test with both reasoning_content and content in the same chunk delta. The current interleaved test alternates the two fields across separate chunks, but providers may emit both in a single delta. When both are present, two normalizeStreamingTextDelta calls execute on the same chunk against independent state objects.

— glm-5.1 via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Independent re-review against the latest commit (fc2492c). Tests + typecheck + lint all clean locally; the iterative refinement (b2da918 / 136017f / fc2492c) has resolved the bulk of earlier critical concerns. Posting a few residual observations as inline comments — none are blockers in isolation, but #1 (prefix-overlap floor) is the one I'd most like to see addressed before merge.

}

if (
rawDelta.startsWith(state.emittedText) &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Prefix-overlap entry still has no minimum-length floor.

The exact-repeat branch below (line 124) requires ≥20 chars before locking cumulative mode, but this branch fires on any prefix overlap of any length. The defense — "a standard OpenAI-protocol incremental provider only sends new tokens, so a chunk would never start with prior accumulated text" — is reasoning from spec, not from observed behavior across all OpenAI-compat backends.

If any provider ever batches tokens unevenly such that a newly batched chunk happens to begin with the prior chunk text (re-send of last token + extra, retry chunk, proxy retransmit, etc.), this branch silently flips the stream into cumulative mode and the very next chunk gets stripped to its suffix relative to a corrupted baseline. That's a hard-to-reproduce regression.

Adding a defensive floor — e.g. require state.emittedText.length >= 8 before allowing prefix-based entry — costs nothing on real cumulative streams (DashScope-class providers exhibit the pattern repeatedly across many chunks; one chunk later the threshold is met) but materially shrinks the false-positive window.

if (
  state.emittedText.length >= CUMULATIVE_DELTA_PREFIX_MIN_BASELINE &&
  rawDelta.startsWith(state.emittedText) &&
  rawDelta.length > state.emittedText.length
) {
  // enter cumulative mode
}

return suffix;
}

if (state.emittedText.startsWith(rawDelta)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This silence path has no debugLogger.debug trace, unlike the other two cumulative-mode transitions.

The prefix-extension entry (line 117), exact-repeat entry (line 126), and cumulative-mode exit (line 100) all log. But this branch — "new chunk is a strict prefix of accumulated text, drop it" — silently returns ''. Network retransmits, proxy retries, and provider-side reconnect resends are exactly the scenarios where a developer needs to know whether this branch fired.

One line completes the observability story:

if (state.emittedText.startsWith(rawDelta)) {
  debugLogger.debug(
    `normalizeStreamingTextDelta: dropping shorter cumulative re-send (${rawDelta.length}b ≤ ${state.emittedText.length}b)`,
  );
  return '';
}

}

if (
rawDelta.startsWith(state.emittedText) &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] After emittedText freezes at the 1024-byte cap, this prefix check (and the exact-repeat check on line 123) keep running against the stale baseline.

For a long incremental stream where some later chunk happens to begin with that exact frozen 1024-byte prefix, cumulative mode falsely engages mid-stream. Constructible scenarios: regenerated boilerplate code, repeated template rendering, agent retries.

Two defensive patterns, either is fine:

  1. Skip cumulative detection once the cap is reached and cumulativeMode is still false — rationale: real cumulative providers exhibit the pattern from chunks 2–3, not after 1KB of clean incremental output.
  2. Track a frozen flag separately and gate the entry checks behind !frozen.
// Option 1 — minimal change
const capped = state.emittedText.length >= CUMULATIVE_DETECTION_WINDOW_BYTES;
if (
  !capped &&
  rawDelta.startsWith(state.emittedText) &&
  rawDelta.length > state.emittedText.length
) {
  // ...
}
if (!capped && rawDelta === state.emittedText) {
  // ...
}

}

if (state.emittedText.length < CUMULATIVE_DETECTION_WINDOW_BYTES) {
state.emittedText += rawDelta;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Test gap — no regression test for the 1024-byte cap itself.

A unit test that streams >1024 bytes of pure incremental chunks and asserts that subsequent prefix-overlap detection still produces the correct verbatim output would lock the cap behavior against accidental future tweaks. Equally, a boundary test at exactly 19/20 chars for CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH would pin that threshold.

These are cheap to add and would catch threshold-tweak regressions that the existing tests miss.

// Some OpenAI-compatible providers send accumulated content in each
// delta.content field. Normalize that shape to incremental suffixes before the
// Gemini stream layer appends it to the live transcript.
function normalizeStreamingTextDelta(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion / non-blocking] normalizeStreamingTextDelta + the two constants + the StreamingTextDeltaState type are self-contained and stateful. converter.ts is already 1300+ lines.

Moving this block to a new streamingDeltaNormalizer.ts (with its own focused unit tests) would:

  • isolate the lifecycle contract — the "per-stream, must not be reused" warning becomes a module-level invariant rather than a JSDoc on two field names buried in RequestContext;
  • shrink the surface of converter.ts;
  • make the test file naturally narrower than the current 80-test omnibus.

Not blocking — fine to land as-is and refactor in a follow-up.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Reasoning channel lacks cumulative mode exit and exact-repeat entry tests. The content channel has dedicated tests (should exit cumulative mode, should resume prefix detection cleanly after exiting, should ignore repeated cumulative chunks) but the reasoning channel only has basic suffix normalization. While both share normalizeStreamingTextDelta, the integration points differ (separate state objects, separate push logic), so the reasoning channel's cumulative edge cases are untested.

[Suggestion] CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH = 20 boundary untested. Existing tests cover well below ('ha', 2 chars) and well above (47 chars) but not 19 vs 20. A threshold change could alter behavior without test failures.

return suffix;
}

if (state.emittedText.startsWith(rawDelta)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Rewind branch returns '' without updating state, risking text duplication on provider backtrack+divergence.

In cumulative mode, when emittedText.startsWith(rawDelta) (provider sends a shorter prefix), this branch returns '' but leaves state.emittedText unchanged. If the next chunk diverges from the stale longer emittedText, cumulative mode exits and the new chunk is emitted verbatim — duplicating the previously-emitted suffix.

Suggested change
if (state.emittedText.startsWith(rawDelta)) {
if (state.emittedText.startsWith(rawDelta)) {
debugLogger.debug(
'normalizeStreamingTextDelta: provider backtracked to shorter prefix, resetting baseline',
);
state.cumulativeMode = false;
state.emittedText = rawDelta;
return '';
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This was already addressed in commit d132acb (pushed before this auto-review). The cumulative-rewind branch in converter.ts now emits a debugLogger.debug trace mirroring the other two suppression paths, so all three silent-suppression sites have parity:

  • entry via prefix-overlap
  • entry via exact-repeat
  • rewind suppressioncumulative rewind suppression (emitted=Nb, chunk=Mb)

Live source (line 97-99):

if (state.emittedText.startsWith(rawDelta)) {
  debugLogger.debug(
    `normalizeStreamingTextDelta: cumulative rewind suppression (emitted=${state.emittedText.length}b, chunk=${rawDelta.length}b)`,
  );
  return '';
}

The duplication-on-divergence scenario you described is also covered by the existing rewind-then-extension test (should suppress cumulative rewind (provider re-sends shorter accumulated string)), which verifies that after a rewind the provider can resume extension without spurious duplicates. No code change needed.

rawDelta.startsWith(state.emittedText) &&
rawDelta.length > state.emittedText.length
) {
const prevLen = state.emittedText.length;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Prefix-overlap heuristic has no minimum-length floor for entering cumulative mode, making it vulnerable to false-positive on short coincidental prefix extensions from a non-cumulative provider via a reassembling proxy.

The exact-repeat branch below (line 124) requires ≥20 chars before locking cumulative mode, but this prefix-overlap branch fires on any prefix extension regardless of length. Consider adding a minimum emittedText length threshold (e.g., 50 chars) before allowing prefix-overlap entry, reducing false-positive probability while still detecting genuine cumulative providers that emit full sentences.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-examining this one in the post-d132acb97 state.

You're right that the prefix-overlap entry path has no min-length floor, in contrast to the exact-repeat branch which requires >= CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH (20). I want to keep the asymmetry rather than add a floor here, and surface the reasoning so a future maintainer can revisit:

Why the two branches are intentionally asymmetric:

  1. Exact-repeat (rawDelta === emittedText) is high false-positive risk on its own — a non-cumulative provider that repeats a 2-char word like "ha" or a punctuation token is the most common collision pattern. Hence the 20-char floor.
  2. Prefix-overlap (rawDelta.startsWith(emittedText) && rawDelta.length > emittedText.length) requires the new chunk to literally start with the entire previously-emitted text. For a non-cumulative provider whose chunks are independent tokens, this collision needs the new chunk to coincidentally reproduce the full transcript so far as its prefix — which is exponentially less likely as the transcript grows.

What's protecting it instead:

  • CUMULATIVE_DETECTION_WINDOW_BYTES = 1024 (added in fc2492c) — once 1024 bytes of non-cumulative content have accumulated, the baseline freezes and the prefix-overlap check early-returns before evaluating. So the collision window for accidental prefix lock-in is bounded to the first 1024 bytes of a stream.
  • The proxy-reassembly scenario you describe could in principle land inside that window. The current cost when it does: one false positive locks the stream into cumulative mode, and the next chunk that does not prefix-extend triggers cumulative-mode exit (b2da918) — which resets the baseline to that chunk and emits it verbatim. So the worst-case observable damage is: one suppressed chunk between the false-positive entry and the next non-prefix-extension chunk.

Bottom line / forward pointer: if we ever see a real-world report of a reassembling proxy poisoning early prefix detection, the next step is to add a small min-length floor on prefix-overlap entry (e.g. prevLen >= 8) symmetric with the exact-repeat floor, rather than dropping prefix detection entirely. Until then, the window cap + exit-and-recover path keep the blast radius bounded.

Deferring without a code change — happy to revisit if a concrete failing trace shows up.

requestContext,
Boolean(choice.finish_reason),
),
const normalizedContent = normalizeStreamingTextDelta(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Missing test: empty normalizedContent (returned '') combined with finish_reason: 'stop'. All 8 new cumulative tests use finish_reason: null, so the normalizedContent || choice.finish_reason guard's empty-string path is untested in a cumulative context. This path matters because it flushes buffered tagged-thinking content on stream end even when the delta itself is silenced.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in commit c4ac09f with a new test: should still call into convertOpenAITextToParts on finish_reason when the cumulative-mode normalized delta is empty.

The test:

  1. Sends a 2-chunk prefix-extension pair to establish cumulative mode and prime emittedText.
  2. Sends a final chunk that exact-repeats the accumulated string with finish_reason: 'stop'. The cumulative branch normalizes the delta to '', exercising the normalizedContent || choice.finish_reason guard's empty-string + non-null-finish_reason path.
  3. Asserts finishReason === 'STOP' propagates and no spurious text part is emitted.

This guards the path you flagged: empty normalized delta in cumulative mode must still drive convertOpenAITextToParts on stream end so any buffered tagged-thinking content gets flushed. Test count is now 98 (was 97); local run is green.

expect(emitted[2]).toBe(' there, how are you today?');
});

it('should normalize cumulative reasoning_content deltas to suffixes', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Duplicate test name — nearly identical to the test at line ~2006 ('should normalize cumulative streaming reasoning_content deltas to suffixes' vs 'should normalize cumulative reasoning_content deltas to suffixes'). The names differ by only the word streaming, making it unclear what each uniquely covers. Consider renaming the second test to describe its distinct scenario (e.g., multi-line step-by-step reasoning).

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in commit c4ac09f. Renamed the test at line 2162 to clarify its distinct scenario:

should normalize cumulative reasoning_content deltas across multi-line growth (newline-prefixed suffixes)

The differentiator vs. the test at line 2006 (should normalize cumulative streaming reasoning_content deltas to suffixes) is now explicit in the title and reinforced by an in-test comment: the multi-line variant grows the accumulated text across \n boundaries so the emitted suffixes themselves begin with \n, exercising the slice arithmetic at the newline. The single-line variant covers the basic intra-sentence prefix extension. Both are kept since they exercise non-overlapping arithmetic edge cases on the same channel.

…n window cap

Adds reasoning-channel coverage requested in the most recent review of the
cumulative-delta normalization fix:

- exact-repeat entry on `reasoning_content` (mirrors the content-channel
  `should ignore repeated cumulative chunks` test)
- cumulative-mode exit on `reasoning_content` when a chunk does not match
  the prior accumulated text (mirrors `should exit cumulative mode`)
- prefix-detection re-entry on `reasoning_content` after the exit path
  resets the baseline (mirrors `should resume prefix detection cleanly`)

Also lands the previously-staged converter.ts hardening from the
2026-05-08 review round and adds the missing detection-window cap test:

- emit `debugLogger.debug` trace on the cumulative-rewind suppression
  branch so the third silent-suppression path has parity with the other
  two (was the only suppression path with no observability)
- early-return when the non-cumulative `emittedText` baseline is frozen
  at `CUMULATIVE_DETECTION_WINDOW_BYTES` (1024); prevents prefix/exact-repeat
  checks from running against a stale baseline once the cap is reached
- regression test: 2000 chars of incremental chunks past the 1024 cap all
  pass through verbatim (the cap holds; no late misclassification)

Reasoning and content channels share `normalizeStreamingTextDelta` but
maintain separate state objects, so the integration points differ; these
tests guard against future refactors that accidentally couple them or
break the per-channel exit/re-entry behavior.

Generated with AI

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

chiga0 commented May 9, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed in commit d132acb.

Reasoning channel cumulative-mode mirror tests (your suggestion 1):

Three new tests on the reasoning channel mirroring the content channel coverage you flagged:

  1. should ignore repeated cumulative reasoning_content chunks with no new suffix — exact-repeat entry path on the reasoning state
  2. should exit cumulative mode on reasoning_content channel when chunk does not match prior accumulated text — exit path resets the reasoning baseline correctly
  3. should resume prefix detection on reasoning_content channel after exiting cumulative mode — re-entry into cumulative mode after a baseline reset

Although both channels share normalizeStreamingTextDelta, they have separate state objects and separate push logic at the call sites — these tests guard against future refactors that accidentally couple them or break per-channel exit/re-entry.

20-char threshold boundary (your suggestion 2):

Already covered by the existing tests added in commit fc2492c:

  • should enter cumulative mode on exact 20-char repeat (at threshold) — exactly 20 chars, second chunk emits '', third extends and emits suffix
  • should pass through 19-char exact repeat without entering cumulative mode (below threshold) — exactly 19 chars, both pass verbatim, then a longer prefix-extension triggers entry

This pair pins the >= versus > behavior on the threshold; off-by-one in either direction would fail one of the two.

Bonus — also picked up two stragglers from the 2026-05-08 review round in the same commit:

  • debugLogger.debug trace on the cumulative-rewind suppression path (was the only suppression path with no observability — addresses 3207794761)
  • early-return when emittedText baseline is frozen at the 1024-byte cap, so prefix/exact-repeat checks can't run against a stale baseline (addresses 3207794781) + new should not enter cumulative mode after the detection window cap is reached on a non-cumulative stream regression test

Tests + typecheck + lint clean locally (vitest run packages/core/src/core/openaiContentGenerator/converter.test.ts → 97 passed).

@chiga0 chiga0 requested review from wenshao and yiliang114 May 9, 2026 08:09

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All findings from this review overlap with existing unresolved inline comments (converter.ts:96, converter.test.ts:2162). No new comments to add. — deepseek-v4-pro via Qwen Code /review

@wenshao

wenshao commented May 9, 2026

Copy link
Copy Markdown
Collaborator

三处再处理一下就可以 approve:

  1. converter.ts:122@yiliang114 的讨论里选 (a) 加 ≥50 字节最低基线(保留自动检测,只是把误判窗口缩小,也跟我前几轮提的 prefix-overlap 阈值是同一件事)。
  2. converter.ts:96 rewind 分支补 state 重置——目前严格前缀 chunk 返回 ''emittedText 不动,后续 chunk diverge 时会触发 exit 路径把新 chunk verbatim 输出,可能与已 emit 的内容产生重复。
  3. converter.test.ts:2162 那两个 reasoning_content 测试名(should normalize cumulative streaming reasoning_content deltas to suffixes vs should normalize cumulative reasoning_content deltas to suffixes)只差一个词,请按各自覆盖的场景改成可区分的描述。

其余 thread 已经在前几轮 commit 里 "Adopted/Fixed in commit X" 落地,麻烦顺手 Resolve 一下。

…icate

Two follow-ups from wenshao's 2026-05-09 review:

1. Add coverage for the `normalizedContent || choice.finish_reason` guard
   on the content path in cumulative mode. All prior cumulative tests use
   `finish_reason: null`, so the empty-normalized + non-null finish_reason
   path was untested. Establishes cumulative mode, then sends an exact-
   repeat final chunk with `finish_reason: 'stop'`; asserts the empty
   normalized delta does not emit spurious text but `finishReason` still
   propagates through to the candidate.

2. Rename the second cumulative-reasoning_content test (line 2162) to
   clarify its distinct scenario — multi-line accumulation where the
   emitted suffix itself begins with a newline. The previous name was
   confusingly similar to the test at line 2006 (single-line cumulative
   reasoning), making it unclear what each uniquely covered.

97 → 98 tests; lint clean.

Generated with AI

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

chiga0 commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Re: the 2026-05-09T17:09 review ("All findings from this review overlap with existing unresolved inline comments (converter.ts:96, converter.test.ts:2162). No new comments to add."):

Both anchors have now been resolved across two commits:

converter.ts:96 (rewind branch trace + downstream concerns at converter.ts:88, :111, :122) — resolved in d132acb (debugLogger trace on the rewind branch + detection-window cap early-return).

converter.test.ts:2162 (duplicate test name + missing boundary coverage) — initial boundary coverage landed in d132acb; the rename and the empty-cumulative + finish_reason coverage landed in c4ac09f just now.

Inline reply trail for every thread cited by the auto-review is now updated; nothing is sitting unanswered.

Local verification (npx vitest run packages/core/src/core/openaiContentGenerator/converter.test.ts against c4ac09f):

 RUN  v3.2.4 .../upgrade-ink-7

 ✓ |@qwen-code/qwen-code-core| src/core/openaiContentGenerator/converter.test.ts (98 tests) 10ms

 Test Files  1 passed (1)
      Tests  98 passed (98)

Lint also clean. Ready for re-review — please dismiss the CHANGES_REQUESTED token if the resolutions above land cleanly on your end.

@chiga0 chiga0 requested a review from wenshao May 11, 2026 02:06
@chiga0

chiga0 commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

All @wenshao's review feedback addressed across d132acb + c4ac09f: reasoning channel cumulative-mode coverage (3 mirror tests), CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH=20 boundary at 19 vs 20, 1024-byte window cap protection test, empty cumulative + finish_reason path, duplicate test name renamed. 98/98 converter tests passing, CI all green, mergeable. Could you take another look?

state.cumulativeMode = false;
// Reset baseline to current chunk so future prefix checks use fresh state.
state.emittedText = rawDelta;
return rawDelta;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The CUMULATIVE_DETECTION_WINDOW_BYTES early-return guard fires before the prefix-extension and exact-repeat checks. If a cumulative provider sends a first chunk larger than 1024 chars, emittedText immediately exceeds the cap. On the second chunk the guard returns rawDelta verbatim, permanently disabling cumulative detection for the entire stream and causing massive text duplication.

Suggested change
return rawDelta;
// Move the cap guard (lines 109-113) so it gates only the fallback concatenation
// path (line 135-138), not the prefix-extension/exact-repeat detection. The cap
// should prevent emittedText growth in non-cumulative fallback, not gate
// cumulative detection itself.

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and fixed in a21ac59.

Walked the control flow with a 1500-char first cumulative chunk:

  1. Chunk 1 (1500 chars): emittedText.length === 0 → first-chunk path sets emittedText = rawDelta (1500 chars), returns chunk.
  2. Chunk 2 (1700 chars, cumulative): cumulativeMode = false, emittedText.length (1500) >= 1024 → early-return guard fires → returns chunk verbatim. First 1500 chars duplicated. Bug confirmed.

The window cap's original goal (bounding per-stream memory for non-cumulative streams) is already enforced by the concat guard at the fallback path (emittedText += rawDelta only runs below the cap). The early-return at the detection path was redundant defense-in-depth and harmful to real cumulative streams with large initial chunks.

Fix: dropped the early-return guard on the detection path. Prefix-overlap and exact-repeat checks now always run; only the fallback concat path is gated by the cap. Added a regression test (should detect cumulative mode even when the first chunk exceeds the detection window cap) with 1500/1700/1750-char chunks. The existing "cap is reached on a non-cumulative stream" test still passes because incremental chunks (20 chars) never satisfy the rawDelta.length > emittedText.length (1024) strict-longer check anyway.

rawDelta.startsWith(state.emittedText) &&
rawDelta.length > state.emittedText.length
) {
const prevLen = state.emittedText.length;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Swap the conditions so the cheaper length check runs first. When rawDelta.length <= state.emittedText.length, startsWith is always false but incurs call overhead.

Suggested change
const prevLen = state.emittedText.length;
if (
rawDelta.length > state.emittedText.length &&
rawDelta.startsWith(state.emittedText)
) {

— deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied in a21ac59. The conditions are now ordered length-first so startsWith is skipped when rawDelta cannot possibly be a strict extension.

… cap

Address wenshao's [Critical] review on PR #3896: the
CUMULATIVE_DETECTION_WINDOW_BYTES early-return guard was firing before the
prefix-overlap and exact-repeat checks, so cumulative providers whose first
chunk exceeds the 1024-byte cap had detection permanently disabled, causing
the entire first chunk to be duplicated on the second chunk.

The cap's original goal — bounding per-stream memory for non-cumulative
streams — is already enforced by the concat guard at the fallback path
(`emittedText += rawDelta` runs only below the cap). The early-return guard
on the detection path was redundant defense-in-depth and harmful to real
cumulative streams with large initial chunks.

Also applies the [Suggestion] from the same review pass: swap the
prefix-overlap conditions so the cheap length check runs before
`startsWith`, avoiding the call when `rawDelta` cannot be a strict
extension.

Adds a regression test covering the cumulative-first-chunk-large case
(1500/1700/1750-char chunks) to prevent re-introduction.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0 chiga0 requested a review from wenshao May 12, 2026 07:11
expect(emitted[2]).toBe(' extra');
});

it('should not enter cumulative mode after the detection window cap is reached on a non-cumulative stream', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] 测试名称/注释与实际测试内容不符。测试声称验证"以冻结的 1024 字符基线开头的后续 chunk 不会被误分类为累积模式",但测试数据(100 个互不相同的增量 chunk,如 chunk000-payload__)中没有任何 chunk 以之前累积的文本开头。它实际只验证了一个较弱的断言:超过上限后普通增量 chunk 能正确透传,并未验证前缀检查与冻结 emittedText 之间的交互行为。

建议扩展测试数据,在 1024 字符边界之后添加一个恰好以冻结前缀开头的新文本 chunk,验证其被逐字透传而不误入累积模式。

Suggested change
it('should not enter cumulative mode after the detection window cap is reached on a non-cumulative stream', () => {
// 在 1024 字符边界之后的 chunk 列表中,添加一条以冻结基线开头的新 chunk:
// 例如在前 51 个 chunk 累积到 1020+ 字符后,第 52 个 chunk 以已累积文本开头
// const chunks = [...]; chunks.push(`chunk051-payload__extra_content_that_starts_with_frozen_prefix`);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict: legitimate test-quality issue, confirmed. Addressed in commit cdc7b6b.

You're right on both counts:

  1. The fixture (100 distinct chunk###-payload__ chunks) has no internal prefix overlap, so prefix/exact-repeat detection never fires regardless of the cap — the comment promised a "trailing chunk that ... starts with what would be the frozen 1024-char baseline" but no such chunk exists.
  2. Investigating this revealed something more interesting: the cap is not an early-return guard against misclassification. It only bounds emittedText memory growth. A trailing chunk that genuinely overlaps with the frozen 1026-byte baseline would still enter cumulative mode via the prefix-overlap branch at lines 112–124 (verified by simulation: state.cumulativeMode = true, suffix-only emission). So the original test name's stronger claim wasn't backed by the implementation either.

Fix taken: renamed to should pass incremental chunks through verbatim past the detection window cap (none of them overlap) and rewrote the comment to (a) describe what the fixture actually exercises (incremental passthrough past the cap, guards against future regressions that might break the passthrough branch once emittedText.length >= 1024) and (b) explicitly call out the case it does NOT cover (a ≥1024-byte exact-prefix coincidence on a true incremental stream — vanishingly unlikely in practice but undefended). Did not add a "counterexample" test that would assert the misclassification-after-cap behavior, since that would either lock in current (arguably incorrect) behavior or require a real defense-in-depth change that's out of scope for this PR.

All 99 tests in converter.test.ts still pass.

The test previously claimed to verify that a trailing chunk arriving
after the cap "would have" started with the frozen 1024-byte baseline,
but no such chunk existed in the fixture. The 100 distinct incremental
chunks never overlapped each other, so prefix/exact-repeat detection
never fired regardless of the cap. Rename and re-comment the test to
reflect the scenario it actually exercises (incremental passthrough past
the cap) and explicitly call out the case it does NOT cover.

Generated with AI

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

chiga0 commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

All feedback through round 5 addressed and pushed:

  • Round-4 cumulative cap window fix (a21ac59): removed redundant early-return that caused mis-classification on first chunk >1024 chars; added regression test.
  • Round-5 test naming/coverage clarity (cdc7b6b): renamed the cap-frozen-baseline test and rewrote its comment to honestly describe what the fixture exercises; explicitly noted the case NOT covered.

99/99 converter tests passing, CI green, mergeable. Ready for re-review.

@chiga0 chiga0 requested a review from wenshao May 12, 2026 12:34

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅ — DeepSeek/deepseek-v4-pro via Qwen Code /review

@wenshao

wenshao commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Overview

This PR addresses a long-standing class of duplicate-streaming bugs (#3279, #1184, #3838, #3806) where certain OpenAI-compatible upstreams (notably DashScope / 阿里云百炼 Coding Plan) ship delta.content as accumulated full text rather than incremental suffixes. The Gemini pipeline appends each delta verbatim, so the same content concatenates repeatedly during live streaming (~14× amplification on long Markdown tables, per the sentinel data in #3663).

The fix introduces normalizeStreamingTextDelta in converter.ts: a per-stream state machine that detects cumulative-mode chunks (current chunk is a prefix-extension of previously emitted text) and emits only the suffix. The same logic applies independently to delta.content and delta.reasoning_content / delta.reasoning via two distinct state slots on RequestContext. Standard incremental streams flow through untouched (the emittedText baseline is capped at 1024 bytes for non-cumulative streams to bound memory).

Surface area: 3 files, +914 / −8 (786 of the additions are tests).

Strengths

  • Surgical scope. Lives entirely in packages/core, no ink/react coupling — clean separation from the in-flight ink 7 work.
  • State plumbing is sound. Lazy initialization (requestContext.reasoningDeltaState ??= …) keeps state per-request; createRequestContext (pipeline.ts:541) is called fresh per stream, so the JSDoc warning "Must NOT be shared or reused across requests" is structurally enforced.
  • Two channels, two states. Reasoning and content tracked independently — interleaved deltas don't cross-contaminate. The should deduplicate interleaved reasoning_content and content channels independently test guards this.
  • Test coverage is genuinely exhaustive (786 net test lines): happy path, exact-repeat threshold boundaries (19 vs 20 chars), rewind suppression, cumulative-mode exit-and-re-entry, large-first-chunk regression, finish_reason flush on empty normalized delta, baseline non-poisoning by short repeats, multi-line newline-prefixed suffixes, channel independence, dual-channel single-chunk handling.
  • Observability. Every cumulative-mode transition (entry via prefix overlap, entry via exact repeat, exit, rewind suppression) emits debugLogger.debug traces on the existing CONVERTER channel — opt-in, zero cost when disabled.
  • Finish-reason flush is correctly preserved. The guard if (normalizedContent || choice.finish_reason) (converter.ts content-path block) ensures buffered tagged-thinking content still flushes when the final chunk's normalized delta is empty (covered by the should still call into convertOpenAITextToParts on finish_reason … test).

Concerns / suggestions

1. (Minor) Frozen baseline can still match a coincidental 1024-byte prefix

After emittedText hits CUMULATIVE_DETECTION_WINDOW_BYTES (1024) on a non-cumulative stream, the += is skipped — but the prefix-overlap check still runs against that stale baseline. The PR explicitly acknowledges this in the test comment for should pass incremental chunks through verbatim past the detection window cap:

"this test does NOT cover the (currently unhandled) case where a later chunk happens to start with the frozen baseline — that chunk would still trigger prefix-overlap detection against a stale baseline."

In practice the probability of a 1024-byte exact-prefix coincidence on a real incremental stream is vanishingly small, and the acknowledgement is fair. Worth surfacing in code (not just the test comment): a one-line comment near the cap-check or the prefix-overlap branch would help future maintainers spot the trade-off. Not a blocker.

2. (Minor) O(N) prefix check on potentially large baselines

In cumulative mode there is no cap on emittedText growth (correctly so — the cap targets the non-cumulative memory concern). For a long Markdown table streamed cumulatively, emittedText can grow into the hundreds of KB, and every subsequent chunk does a startsWith on that whole prefix. The runtime is per-chunk O(min(rawDelta, emittedText)), totalling roughly O(N · final_size / chunk_count) over the stream. In practice memcmp is fast and chunks usually arrive faster than they can be rendered, so this is unlikely to matter — but if a future provider sends thousands of cumulative chunks for a multi-MB response, this becomes the dominant cost. Worth a perf benchmark eventually; not a blocker for this PR.

3. (Style) Magic numbers are well-commented but worth surfacing

CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH = 20 and CUMULATIVE_DETECTION_WINDOW_BYTES = 1024 are sensible defaults backed by clear rationale in comments. If you anticipate provider-specific tuning, consider exposing them on responseParsingOptions later. Not for this PR.

4. (Minor) Behavior when cumulative mode exits onto unrelated content

The exit path resets emittedText = rawDelta and emits the chunk verbatim. If the upstream truly reset (e.g. a stream error caused a restart), the user sees the previously accumulated text and the new fresh text concatenated visually. That's the safe choice — preserving content over silently dropping it — and matches what the test should exit cumulative mode when a chunk does not match prior accumulated text asserts. Flagging that the UX trade-off is visible duplicate rather than silent loss, which is the right default.

Risk assessment

  • False positive risk (incremental stream wrongly classified as cumulative): The 20-byte threshold on exact-repeat and the strict-prefix-extension check on prefix-overlap make this unlikely. The pathological case is a single coincidental prefix match (e.g. chunk 1 = "I", chunk 2 = "I am", on a poorly-tokenized incremental stream). The strict > length comparison means false positives still produce identical output to the cumulative case, so practical impact is null even when triggered.
  • False negative risk (cumulative stream missed): The 1024-byte cap on emittedText growth was previously short-circuiting detection on cumulative streams with large first chunks — that bug was caught in review and fixed in commit a21ac59, with regression test should detect cumulative mode even when the first chunk exceeds the detection window cap. Good catch.
  • Concurrency: Each request gets its own RequestContext; no cross-stream state. Safe.

Verdict

Approve. Targeted, well-scoped fix for a real high-impact bug class. Test coverage is unusually thorough — most concerns above are documentation/perf-margin notes rather than correctness issues, and the PR has clearly already absorbed multiple rounds of careful review (5 review-driven commits visible in the history). Ship it.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did an end-to-end local verification of this PR (HEAD cdc7b6b0e) — built core, then ran four verification layers: unit tests, scripted algorithm replay, corner-case probes, and a side-by-side comparison against a simulated main to confirm the duplication is actually eliminated.

Verified working ✓

  • All 99 tests pass (98 claimed + 1 I missed in the count).
  • Core fix is end-to-end effective. I simulated a 9-chunk DashScope-style cumulative stream of a 304-byte Markdown table and ran it through both main (chunks appended verbatim) and this PR's converter:
    • main: user-visible output 1578 bytes (5.2× amplification), table header | Syntax | Meaning | appears ⚠️
    • PR: user-visible output 304 bytes (1.0× amplification), table header appears
    • PR output is byte-equal to the original final text. Direction matches the table-sentinel evidence in your description (14× → 1×).
  • Normal incremental streams pass through unchanged — verified by feeding a 13-chunk incremental stream of the same content; output reconstructs the original exactly.
  • Channel separation works — interleaved content + reasoning_content chunks each maintain independent state and don't cross-contaminate.

The fundamental algorithm is sound. However, two corner cases of the heuristic produce real user-visible regressions and need to be addressed before merge.

Blocking findings

1. CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH = 20 causes silent data loss on legitimate repeats

The 20-char threshold is too low. Any provider that legitimately emits a chunk equal to a prior chunk of ≥20 characters will have the second chunk silently suppressed:

chunk 1: "import { foo } from './module';"  // 31 chars
chunk 2: "import { foo } from './module';"  // legitimate repeat (e.g. duplicate import)
// PR output: chunk 1 = full text, chunk 2 = ""   ← user loses the second line

I tested boundaries on the converter directly:

chunk length exact repeat → emitted on 2nd chunk
19 chars "X".repeat(19) "XXXXXXXXXXXXXXXXXXX" ✓ preserved
20 chars "X".repeat(20) "" ⚠️ suppressed
31 chars "import { foo } from './module';" "" ⚠️ suppressed

Realistic triggers: repeated import lines in code generation, repeated emoji sequences, repeated boilerplate (e.g. </div>\n</div>), or duplicated short paragraphs.

This is silent lossy behavior — there's no UI warning, no debug log on the user-facing path. The earlier review thread mentioned ≥50 bytes; please raise to at least 50, probably 80. The cumulative-detection signal it's trying to catch (DashScope replaying a long accumulated buffer) is virtually always hundreds of bytes, so a higher threshold doesn't weaken the catch but dramatically reduces the false-positive surface.

2. The CUMULATIVE_DETECTION_WINDOW_BYTES = 1024 cap can produce visible duplication, not just missed detection

I initially assumed the cap would just disable detection past 1024 B. The actual behavior is worse — when a normal-incremental stream exceeds 1024 B and the upstream then switches to cumulative mode, detection mis-fires and the emitted suffix overlaps content the user has already seen.

Reproduction: 200 incremental chunks (1600 B total accumulated) followed by a single cumulative chunk:

incremental phase: user sees 1600 bytes
                   internal emittedText capped at 1024 bytes
cumulative chunk arrives (1614 bytes = 1600 accumulated + "|CONTINUATION|")
  algorithm: chunk.startsWith(emittedText[:1024]) → true → enter cumulative
  emit suffix = chunk[1024:] = ~590 bytes, including bytes 1024-1600 again
user-visible result: 1600 + 590 = ~576 bytes of duplication

Not the targeted use case (DashScope is cumulative from the start), but any "incremental-then-cumulative" hybrid provider will produce visible duplication of about accumulated_length - cap bytes. Fix: track the total emitted length (just the integer, not the string content) separately from the capped baseline, and use it for the suffix slice when entering cumulative mode. Costs ~8 bytes per request.

Non-blocking observations

3. Diverge-with-overlap can produce visible duplication on cumulative exit

Tested:

chunks: ["ABCDEFGHIJ", "ABCDEFGHIJKL", "ABCDEFGHIJKLMN", "GHIJKLMN_NEW"]
// ── chunk 4 doesn't startsWith and isn't startedBy emittedText
// ── exit cumulative, emit chunk 4 verbatim
user-visible: "ABCDEFGHIJKLMN" + "GHIJKLMN_NEW"
//           → "GHIJKLMN" appears 2× in user output

DashScope-class cumulative providers don't emit half-overlapping chunks mid-stream, so this doesn't bite the targeted bug, but it's worth documenting in normalizeStreamingTextDelta's JSDoc that "exit cumulative" is a verbatim-emit path with no overlap reconciliation, and the algorithm assumes the diverged chunk is fully fresh content.

4. state.emittedText grows linearly with stream length once cumulative mode is entered

Confirmed: 5 cumulative chunks of growing length → emittedText.length === 10125 bytes. Each successful match does state.emittedText = rawDelta, so for a 100 KB final response the buffer holds the full 100 KB. Single-request scoped, so it's bounded by request lifetime, but worth a JSDoc note: "in cumulative mode state.emittedText retains the full accumulated text until request completion." A future optimization (only retain the last N bytes once we're confident in cumulative mode) is possible but not required now.


Recommendation

The core fix is sound and the targeted DashScope cumulative-duplication bug is verifiably eliminated end-to-end. #1 (raise the exact-repeat threshold to ≥50) is a one-line change and prevents silent data loss in legitimate-repeat scenarios. #2 (track emitted length separately from capped baseline) is a small change that closes the hybrid-provider regression. With those two addressed I'll re-approve.

Verification scripts saved locally (/tmp/verify-pr3896-{L2,L2_5,L2_5b,L3}.mjs) — happy to share for the follow-up commit.

…ength

Addresses two e2e-verified findings from wenshao's PR #3896 review
(2026-05-13 CHANGES_REQUESTED) where local replay surfaced real
user-visible regressions in the cumulative-delta heuristic.

1. Bump CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH from 20 to 64. Any
   legitimately repeated chunk of 20+ chars (e.g. duplicate import
   lines, repeated short paragraphs, repeated emoji sequences) was
   silently suppressed by the prior threshold. Cumulative-buffer
   replays are virtually always hundreds of bytes, so the higher
   threshold preserves catch-rate while eliminating the silent-loss
   surface.

2. Track the true user-visible emitted length separately from the
   1024-byte capped baseline. For incremental-then-cumulative hybrid
   streams (200 incremental chunks totalling 1600 bytes followed by a
   cumulative chunk), the prior implementation sliced the suffix from
   byte 1024 of the cumulative chunk and re-emitted ~576 bytes the
   user had already seen. The new emittedLength field captures the
   true total so the slice starts at the user-visible boundary; the
   historical short-repeat-then-extend behaviour is preserved by
   gating the emittedLength-based slice on the baseline actually
   having frozen at the cap.

Also documents the per-stream/per-channel state lifecycle and the
"exit cumulative" verbatim-emit semantics on normalizeStreamingTextDelta.

Generated with AI

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

chiga0 commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the end-to-end verification @wenshao — the simulated DashScope replay and the 1578B→304B amplification numbers are exactly the kind of evidence that made both findings actionable. Pushed d1cdc1e6f addressing #1 and #2; #3/#4 land as JSDoc per your guidance.

#1 — exact-repeat threshold (blocking → fixed)

Raised CUMULATIVE_DELTA_EXACT_REPEAT_MIN_LENGTH from 20 → 64. Sits comfortably above the realistic legitimate-repeat surface you flagged (duplicate 31-char imports, </div>\n</div> boilerplate, repeated emoji sequences) while remaining far below any practical cumulative-buffer replay (always hundreds of bytes for DashScope-class providers), so catch-rate is preserved. Picked 64 over your suggested 50/80 as a balance — it's well above any realistic legit-repeat I could construct, but I'm happy to push higher if you want more headroom. New regression test feeds two identical 31-char import lines plus a third line and asserts all three reach the user verbatim.

#2 — slice from real emitted total, not capped baseline (blocking → fixed)

Added emittedLength: number to StreamingTextDeltaState. It tracks user-visible bytes monotonically (incremented on every passthrough/exit path, including short-exact-repeat passthrough). When entering cumulative mode via prefix-overlap, the suffix is sliced from emittedLength instead of the (possibly frozen) emittedText.length, but only when the cap actually kicked in (baselineLen >= CUMULATIVE_DETECTION_WINDOW_BYTES && emittedLength > baselineLen). Outside that hybrid-after-cap case it falls back to the baseline so the historical short-repeat-then-extend behaviour is preserved (which a couple of existing tests depend on).

Regression test mirrors your reproducer: 200 distinct 8-byte incremental chunks (1600B accumulated, well past the 1024B cap) followed by accumulated + "|CONTINUATION|" (1614B). Asserts the cumulative chunk emits exactly the 14-byte tail — no replay of bytes 1024..1600. Extra costs: one integer per channel state, ~8 bytes per request as you predicted.

#3 — diverge-with-overlap on cumulative exit (non-blocking → JSDoc)

Documented on normalizeStreamingTextDelta: "the 'exit cumulative' path is a verbatim-emit path with no overlap reconciliation: the diverged chunk is assumed to be fully fresh content. Cumulative providers that emit a half-overlapping chunk on exit (not observed on DashScope-class providers) would produce visible duplication on the overlap." Matches your characterisation.

#4emittedText linear growth (non-blocking → JSDoc)

Documented: "In cumulative mode state.emittedText retains the full accumulated text for the request lifetime (worst case: ~final response size, e.g. ~100KB for a long answer). Single-request scoped and bounded by request completion. A future optimization could retain only the last N bytes once cumulative mode is firmly established, but is not required today." Left the optimization out of scope per your note.

Verification

  • All 101 converter tests pass (98 prior + 3 new regression tests targeting pre-release: fix ci #1 import-line and Where is the config saved? #2 hybrid-after-cap).
  • Full packages/core suite: 268 files / 7009 tests pass.
  • tsc --noEmit, eslint, and prettier --check all clean on the touched files.

Happy to receive your /tmp/verify-pr3896-*.mjs scripts if you'd like to re-run them against d1cdc1e6f for a third pass.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-verified d1cdc1e6f end-to-end:

  • 101/101 converter tests pass (3 new regression tests landed)
  • #1: threshold raised 20 → 64; 31-char duplicate import line preserved verbatim ✓
  • #2: 1614B cumulative-after-1600B-incremental now emits exactly 14B (|CONTINUATION|) — the ~576B replay is gone ✓
  • #3 / #4: JSDoc additions match the verbatim-exit and linear-growth semantics
  • End-to-end main vs PR: 5.2× → 1.0× amplification still holds; PR output byte-equal to original

Both blocking findings cleanly resolved. LGTM.

@wenshao wenshao merged commit 720ccac into main May 13, 2026
8 checks passed
TaimoorSiddiquiOfficial added a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request May 13, 2026
…nLM#3896) [upstream cherry-pick]

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
wenshao pushed a commit that referenced this pull request May 17, 2026
Some OpenAI-compatible upstreams (notably specific DashScope / 阿里云百炼 Coding Plan paths) send `delta.content` as accumulated full text instead of incremental suffixes. The Gemini stream pipeline appended every chunk verbatim, so the same content was concatenated repeatedly during streaming.

Adds a per-stream `normalizeStreamingTextDelta` that detects cumulative-mode chunks (current chunk starts with previously emitted text) and emits only the suffix. Once cumulative mode is locked in, exact-repeat (≥64 chars) and prefix-overlap chunks are silenced. Normal incremental streams flow through unchanged.

The fix applies to both `delta.content` and `delta.reasoning_content` / `delta.reasoning`, with separate state slots so a switch between text and reasoning channels does not cause false-positive suppression. A separate `emittedLength` counter tracks user-visible bytes so an incremental-then-cumulative hybrid stream slices the correct suffix even after the 1024-byte detection-window cap kicks in.

Fixes #3279, #1184, #3838, #3806.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants