deepseek-ai/deepseek-harness · error

${name}: retainRatio and retainTokens are mutually exclusive

Error message

${name}: retainRatio and retainTokens are mutually exclusive

What it means

Retention (the verbatim tail kept after compaction) has exactly one form per config scope: a fraction of the model's context window (retainRatio) or an absolute token count (retainTokens). resolveRetention() prefers retainTokens when both are present, so validatePolicy() rejects the combination at load instead of silently ignoring one setting. The rule applies to top-level defaults and to each modelPolicies entry separately.

Source

Thrown at packages/compaction/compaction-basic/src/config.ts:241

  validatePolicy(source, name)
}

/** Validate the fields common to defaults and exact-target partial overrides. */
function validatePolicy(
  config: CompactionPolicyConfig | Record<string, unknown>,
  name: string,
): void {
  const thresholdRatio = config.thresholdRatio
  const retainRatio = config.retainRatio
  const retainTokens = config.retainTokens
  const maxTokens = config.maxTokens
  const compactionRetries = config.compactionRetries
  const maxOverflowRetries = config.maxOverflowRetries
  if (thresholdRatio !== undefined) assertRatio(`${name}.thresholdRatio`, thresholdRatio)
  if (retainRatio !== undefined) assertRatio(`${name}.retainRatio`, retainRatio)
  if (retainTokens !== undefined) assertNonNegativeInteger(`${name}.retainTokens`, retainTokens)
  if (retainRatio !== undefined && retainTokens !== undefined) {
    throw new Error(`${name}: retainRatio and retainTokens are mutually exclusive`)
  }
  if (maxTokens !== undefined) assertPositiveInteger(`${name}.maxTokens`, maxTokens)
  if (compactionRetries !== undefined) {
    assertNonNegativeInteger(`${name}.compactionRetries`, compactionRetries)
  }
  if (maxOverflowRetries !== undefined) {
    assertNonNegativeInteger(`${name}.maxOverflowRetries`, maxOverflowRetries)
  }

  validateSummarizationPair(config, name)
}

/** Require one scope to omit, clear, or replace the summarization target as a pair. */
function validateSummarizationPair(
  config: CompactionPolicyConfig | Record<string, unknown>,
  name: string,
): void {
  const provider = config.summarizationProvider

View on GitHub (pinned to b150a551b8)

Solutions

  1. Pick one retention form per scope: delete retainRatio or delete retainTokens
  2. For absolute budgets keep retainTokens and remove retainRatio; for window-relative budgets do the opposite
  3. If you want per-model absolutes plus a global ratio, keep them in different scopes — entry-level retainTokens overriding top-level retainRatio is valid

Example fix

# before — both forms in the same scope
retainRatio: 0.1
retainTokens: 4000

# after — absolute budget only
retainTokens: 4000
Defensive patterns

Strategy: validation

Validate before calling

for (const scope of [config, ...(config.modelPolicies ?? [])]) {
  if (scope.retainRatio !== undefined && scope.retainTokens !== undefined) {
    throw new Error('choose retainRatio OR retainTokens per config scope')
  }
}
resolveConfig(config)

Try / catch

try {
  resolveConfig(config)
} catch (err) {
  if ((err as Error).message.includes('mutually exclusive')) {
    // The message names the scope: top-level 'BasicCompactionConfig' or 'modelPolicies[i]'.
    throw new Error(
      'retention conflict: keep exactly one of retainRatio / retainTokens in the named scope',
      { cause: err },
    )
  }
  throw err
}

Prevention

When it happens

Trigger: Both retainRatio and retainTokens set in the same scope: the top-level config block, or one modelPolicies entry. Cross-scope mixing is legal — an entry may set retainTokens while the top level sets retainRatio; the entry replaces the inherited retention.

Common situations: Tuning compaction and forgetting an old retainRatio is still present; copying a per-model entry that had retainTokens into a top-level block that already sets retainRatio; assuming the more specific value wins silently.

Related errors


AI-assisted analysis of deepseek-ai/deepseek-harness@b150a551b8 (2026-08-24). Data as JSON: /api/errors/3b3813543933f587. Report an issue: GitHub.