ImageProcessor logs hard-coded format: 'webp' and quality: 80 in its compression stats instead of the configured options

Author: sxh313Created Sep 23, 2026Updated Sep 23, 2026

Component

@tarko/context-engineer — multimodal/tarko/context-engineer/src/node/image-processor.ts

Bug description

ImageProcessor advertises compression statistics as one of its features and exposes a configurable { quality, format } constructor, but the stats line it prints reports two hard-coded constants instead of the values the instance is actually running with:

typescript
// context-engineer/src/node/image-processor.ts @ c2ad42e3
18: * - Provides compression statistics
...
23:  constructor(options: { quality?: number; format?: 'webp' | 'jpeg' | 'png' } = {}) {
24:    this.compressor = new ImageCompressor({
25:      quality: options.quality ?? 5,
26:      format: options.format ?? 'webp',
27:    });
28:  }
...
97:      console.log('Image compression stats:', {
98:        original: formatBytes(originalSize),
99:        compressed: formatBytes(compressedSize),
100:        ratio: `${compressionRatio.toFixed(2)}x (${compressionPercentage}% smaller)`,
101:        format: 'webp',
102:        quality: 80,
103:      });

So the three byte-size fields are measured from the real buffers, while format and quality on the very next two lines are literals that never change.

The 80 is not a value ImageProcessor ever resolves: it is the compressor's own default, while this class's default quality is 5 (:25), and neither field moves when the caller passes options:

typescript
// shared-media-utils/src/ImageCompressor.ts
34:  public readonly options: ImageCompressionOptions;
39:      quality: options?.quality ?? 80,

Quality is not a neutral label here either: getPluginsForFormat() (ImageCompressor.ts:63-80) feeds it straight into the encoder, and reads it back from this.options per format — divided by 100 for png (:64, used at :71-72), passed raw for jpeg (:68) and webp (:76).

Actual vs expected

  • Actual: for new ImageProcessor({ quality: 90, format: 'png' }) the server logs format: 'webp', quality: 80.
  • Expected: the log reports format: 'png', quality: 90, i.e. what the instance compresses with.

How is this actually affecting real users?

Both shipped callers construct the processor with quality: 5:

typescript
// agent-server/src/api/controllers/queries.ts:10-13
const imageProcessor = new ImageProcessor({
  quality: 5,
  format: 'webp',
});

// agent-server-next/src/controllers/queries.ts:17-20 (same options, inside a try/fallback block)

so every image query through the agent server logs quality: 80 while the encoder is being handed 5. Reachability, hop by hop:

  1. The query route: agent-server/src/api/routes/queries.ts:15-17 — router.post('/query', queriesController.executeQuery) inside app.group('/api/v1/sessions', …) (mounted by routes/index.ts:19)
  2. agent-server/src/api/controllers/queries.ts:38 export async function executeQuery
  3. agent-server/src/api/controllers/queries.ts:57 await imageProcessor.compressImagesInQuery(query) (also :116 in executeStreamingQuery, and agent-server-next/src/controllers/queries.ts:87/:162)
  4. context-engineer/src/node/image-processor.ts:45-54 maps each image_url part → :49 this.compressImageUrl(part)
  5. image-processor.ts:97-103 prints the stats line with the two constants.

ImageProcessor is part of the package's public surface (context-engineer/src/node/index.ts:3 export * from './image-processor';), so this is not only reachable through the two servers above. The log line is the only place a caller can see what the processor decided to compress with: ImageCompressor.getOptionsDescription() (ImageCompressor.ts:85-89) does report the live options, but ImageProcessor keeps its compressor in a private field (image-processor.ts:21), so that check is not available either.

Reproduction

The stats line is observable without any encoder binary, because it is emitted after compressToBuffer() resolves — a prototype spy on that one method is enough. Against main @ c2ad42e3 (with shared-media-utils built locally so the package's tests collect), a tests/image-processor.test.ts covering the reported stats fields and the two fallback paths:

$ cd multimodal/tarko/context-engineer && CI=true ./node_modules/.bin/vitest run tests/image-processor.test.ts

 ❯ tests/image-processor.test.ts (7 tests | 3 failed) 35ms
   × ImageProcessor > compression statistics > reports the quality the processor was constructed with 19ms
     → expected 80 to be 5 // Object.is equality
   × ImageProcessor > compression statistics > reports the default quality when constructed without options 2ms
     → expected 80 to be 5 // Object.is equality
   × ImageProcessor > compression statistics > reports the configured format instead of a fixed one 4ms
     → expected 'webp' to be 'png' // Object.is equality

 Test Files  1 failed (1)
      Tests  3 failed | 4 passed (7)

The four passing cases are controls (byte sizes measured from the real buffers, one stats line per image in a query, the original part returned when compression rejects, and non-data-URI URLs left untouched) — they pass before and after, so the fix stays confined to the two reported fields.

Proposed fix

Resolve quality/format once in the constructor into a private readonly options, pass that object to ImageCompressor (identical values to today, so compression behaviour does not change), and log this.options.format / this.options.quality. The source diff is 8 additions / 4 deletions in one file.

Two adjacent problems are deliberately left out of scope, since fixing them needs a decision rather than a correction:

  • image-processor.ts:91 labels the result data:image/webp;base64, regardless of the configured format — another literal. Deriving it from this.options.format would be closer but still not always right: all three plugins return their input unchanged when the bytes do not match what they encode ([email protected]/index.js:11-12 for unreadable input, [email protected]/index.js:15-16 for non-PNG, [email protected]/index.js:18-20 for non-JPEG), so the configured format does not determine the returned bytes. Reporting the produced format needs the compressor to inspect the buffer it hands back (imagemin's own file-based API does this with file-type).
  • image-processor.ts:82 strips the data-URI prefix with /^data:image\/\w+;base64,/, which does not match image/svg+xml because + is outside \w — while the guard at :77 (startsWith('data:image/')) does accept it. Measured with node: for a data:image/svg+xml;base64,… URL the replace() is a no-op, so the 76-byte SVG is decoded from a string that still carries the 26-character data:image/svg+xml;base64, prefix and comes back as 93 different bytes (dec.equals(svg) === false). Those bytes are what gets handed to the encoder and what originalSize is measured from, so an SVG part produces a stats line about a buffer that never existed. Changing the regex alters which parts get compressed, so it belongs in a separate discussion.

Environment

  • Branch/commit: main @ c2ad42e3
  • OS: Windows 11 (the defect is OS-independent — it is two literals in a console.log)

Happy to send the fix as a PR — I plan to work on it myself, so posting this to claim it. Before writing anything I checked all 137 open PRs (file lists, paginated) at 2026-09-23T20:29Z: none touches context-engineer/src/node/image-processor.ts or adds a test for it.

Source: bytedance/UI-TARS-desktop