JuliusBrussee/caveman · error · Error

cave_compaction_option_invalid

cave_compaction_option_invalid

Error message

cave_compaction_option_invalid

What it means

normalizeCompaction merges caller options with defaults, then requires every numeric option (maxCompactions, keepRecentTokens, summaryMaxTokens, minYieldTokens, headroomCalls, pinnedUserTokens) to be a safe positive integer. Any zero, negative, fractional, NaN, Infinity, or non-number value throws immediately.

Source

Thrown at packages/agent/src/compaction.ts:100

  keepRecentTokens: 8_000,
  summaryMaxTokens: 2_048,
  minYieldTokens: 20_000,
  headroomCalls: 3,
  pinnedUserTokens: 20_000,
});

export function normalizeCompaction(options: CompactionOptions = {}): NormalizedCompaction {
  const merged = {
    maxCompactions: options.maxCompactions ?? DEFAULTS.maxCompactions,
    keepRecentTokens: options.keepRecentTokens ?? DEFAULTS.keepRecentTokens,
    summaryMaxTokens: options.summaryMaxTokens ?? DEFAULTS.summaryMaxTokens,
    minYieldTokens: options.minYieldTokens ?? DEFAULTS.minYieldTokens,
    headroomCalls: options.headroomCalls ?? DEFAULTS.headroomCalls,
    pinnedUserTokens: options.pinnedUserTokens ?? DEFAULTS.pinnedUserTokens,
  };
  for (const value of Object.values(merged)) {
    if (!Number.isSafeInteger(value) || value <= 0) {
      throw new Error("cave_compaction_option_invalid");
    }
  }
  return Object.freeze({
    ...merged,
    summarizerModel: options.summarizerModel,
  });
}

/** Version of the summary contract the summarizer is asked to emit. */
export const SUMMARY_SCHEMA_VERSION = 1;

/**
 * The sectioned summary the summarizer must produce.
 *
 * Structure rather than prose keeps required fields explicit. `constraintsRestated`
 * is a restatement only — the pinned buffer is the carrier, and a summary that
 * becomes the only carrier reproduces the failure this design exists to avoid.
 */

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set every option to a positive whole number: keepRecentTokens: 4096, maxCompactions: 3, etc.
  2. Unquote numbers in config files and coerce parsed strings with Number() before passing, checking Number.isSafeInteger
  3. To effectively disable compaction, use a very large (but valid) budget rather than 0

Example fix

// before
normalizeCompaction({ keepRecentTokens: 0, summaryMaxTokens: 2048.5 });

// after
normalizeCompaction({ keepRecentTokens: 1024, summaryMaxTokens: 2048 });
Defensive patterns

Strategy: validation

Validate before calling

function assertCompactionOptions(o: Record<string, unknown>): void {
  for (const [k, v] of Object.entries(o)) {
    if (!Number.isSafeInteger(v) || (v as number) <= 0) {
      throw new Error(`compaction option ${k} must be a positive safe integer, got ${String(v)}`);
    }
  }
}

Type guard

function isPositiveSafeInteger(v: unknown): v is number {
  return typeof v === "number" && Number.isSafeInteger(v) && v > 0;
}

Prevention

When it happens

Trigger: Passing CompactionOptions with e.g. keepRecentTokens: 0, summaryMaxTokens: 1.5, maxCompactions: -1, or a value parsed from user config as a string ('4096'), or undefined leaking through arithmetic that produced NaN.

Common situations: Config files where numbers were quoted; env-var parsing with parseInt returning NaN on bad input; 'disable compaction' attempts expressed as maxCompactions: 0; float math producing fractional token counts.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/e7233c7190bfab88. Report an issue: GitHub.