feat(compaction): make conversation compaction configurable per model preset #338

Merged
rwachters merged 4 commits from feat/model-preset-compaction-config into master 2026-10-08 18:41:45 +00:00
rwachters commented 2026-10-08 18:17:22 +00:00 (Migrated from github.com)

Summary

Makes conversation compaction configurable per model preset. A preset gains a compaction_enabled flag and an optional compaction_threshold_tokens override, and the effective configuration is resolved and validated once per turn, inside the turn-preparation transaction, instead of being read mid-turn from the user preference alone. A preset can only restrain compaction (its flag is ANDed with the user preference) and override the threshold; the auxiliary summarization model, settings, instruction and summary label stay user-preference-owned.

Changes

  • Added the additive V35__model_preset_compaction_config.sql migration plus the table, entity, mapper, DAO and service plumbing for the two new preset columns, defaulting existing rows to enabled with no override so current behaviour is preserved.
  • Added compactionEnabled (default true) and compactionThresholdTokens (default null) to ModelPresetDto, the create/update preset requests and the built-in preset tool schemas, and mapped create/update failures to typed errors.
  • Added the app-side preset form controls (a switch and an optional threshold where blank means "use the user preference threshold"), the detail-panel/list-item rendering of the resulting configuration, and their tests.
  • Added ResolvedCompactionConfig / EffectiveCompactionSettings and DefaultEffectiveCompactionConfigResolver, which resolve and validate the preset and the stored preference inside the turn's transaction; the value is carried through PreparedConversationTurn → ConversationTurnRequest → ConversationCompactionService.beginTurn.
  • Effective enablement is preset.compactionEnabled AND a present preference row AND preference.enabled; a preset with compaction disabled short-circuits before the preference is read or decoded. Effective threshold is the preset override when set, otherwise the preference threshold.

Behaviour changes

  • B1: A stored non-positive preference thresholdTokens now fails the turn at validation time instead of only when compaction ran, and it can no longer be masked by a preset threshold override.
  • B2: A compaction-enabled preset combined with a preference whose auxiliary modelId/settingsId is null now fails every turn of that session instead of running until the window exceeded the threshold.
  • B3: Non-positive ids and a blank instruction now fail up front rather than mid-turn; this can only affect legacy or hand-edited preference rows.
  • B4: A malformed stored preference is reported at validation time as MODEL_CONFIGURATION_ERROR with nothing persisted, instead of a mid-turn CompactionFailed after the user message had already been saved.

API contract change

  • PUT /api/v1/me/preferences/conversation_compaction now rejects null auxiliary ids with 400 invalid_argument while enabled == true; null ids remain valid while enabled == false.
  • Because enabled defaults to true, a PUT that omits enabled and sends null ids is now rejected.

Implementation notes

  • The configuration is intentionally snapshotted once per turn: chunks already persisted keep the threshold that was in effect when they were created.
  • Only the preset's enablement flag and threshold cross into the turn; the auxiliary summarizer configuration remains a user preference, so there is a single place to change the summarization model.
  • ConversationCompactionService.beginTurn no longer returns an Either: the configuration was already resolved and validated during preparation, so the remaining failure modes surface from preparePrimaryContext only when compaction is actually needed.

Validation

  • ./gradlew :server:test — focused filters for the new resolver, the compaction configuration service and resolver, turn preparation, the orchestrator suites, the preset routes/DAO/service/tools and the migrator test were run green.
  • ./gradlew :app:desktopTest — preset form state, form dialog and API client suites were run green.
  • ./gradlew :common:desktopTest — preset request serialization and built-in tool catalog suites were run green.

Risks and follow-up

  • The preference PUT is the only intentionally breaking part of this change; clients that stored null auxiliary ids with enabled omitted or true must now set enabled: false or provide ids.
  • The two new columns are additive with defaults, so pre-existing rows need no data update; downgrade is not supported.
## Summary Makes conversation compaction configurable per model preset. A preset gains a `compaction_enabled` flag and an optional `compaction_threshold_tokens` override, and the effective configuration is resolved and validated once per turn, inside the turn-preparation transaction, instead of being read mid-turn from the user preference alone. A preset can only restrain compaction (its flag is ANDed with the user preference) and override the threshold; the auxiliary summarization model, settings, instruction and summary label stay user-preference-owned. ## Changes - Added the additive `V35__model_preset_compaction_config.sql` migration plus the table, entity, mapper, DAO and service plumbing for the two new preset columns, defaulting existing rows to enabled with no override so current behaviour is preserved. - Added `compactionEnabled` (default `true`) and `compactionThresholdTokens` (default `null`) to `ModelPresetDto`, the create/update preset requests and the built-in preset tool schemas, and mapped create/update failures to typed errors. - Added the app-side preset form controls (a switch and an optional threshold where blank means "use the user preference threshold"), the detail-panel/list-item rendering of the resulting configuration, and their tests. - Added `ResolvedCompactionConfig` / `EffectiveCompactionSettings` and `DefaultEffectiveCompactionConfigResolver`, which resolve and validate the preset and the stored preference inside the turn's transaction; the value is carried through `PreparedConversationTurn` → `ConversationTurnRequest` → `ConversationCompactionService.beginTurn`. - Effective enablement is `preset.compactionEnabled` AND a present preference row AND `preference.enabled`; a preset with compaction disabled short-circuits before the preference is read or decoded. Effective threshold is the preset override when set, otherwise the preference threshold. ## Behaviour changes - B1: A stored non-positive preference `thresholdTokens` now fails the turn at validation time instead of only when compaction ran, and it can no longer be masked by a preset threshold override. - B2: A compaction-enabled preset combined with a preference whose auxiliary `modelId`/`settingsId` is null now fails every turn of that session instead of running until the window exceeded the threshold. - B3: Non-positive ids and a blank instruction now fail up front rather than mid-turn; this can only affect legacy or hand-edited preference rows. - B4: A malformed stored preference is reported at validation time as `MODEL_CONFIGURATION_ERROR` with nothing persisted, instead of a mid-turn `CompactionFailed` after the user message had already been saved. ## API contract change - `PUT /api/v1/me/preferences/conversation_compaction` now rejects null auxiliary ids with `400 invalid_argument` while `enabled == true`; null ids remain valid while `enabled == false`. - Because `enabled` defaults to `true`, a `PUT` that omits `enabled` and sends null ids is now rejected. ## Implementation notes - The configuration is intentionally snapshotted once per turn: chunks already persisted keep the threshold that was in effect when they were created. - Only the preset's enablement flag and threshold cross into the turn; the auxiliary summarizer configuration remains a user preference, so there is a single place to change the summarization model. - `ConversationCompactionService.beginTurn` no longer returns an `Either`: the configuration was already resolved and validated during preparation, so the remaining failure modes surface from `preparePrimaryContext` only when compaction is actually needed. ## Validation - `./gradlew :server:test` — focused filters for the new resolver, the compaction configuration service and resolver, turn preparation, the orchestrator suites, the preset routes/DAO/service/tools and the migrator test were run green. - `./gradlew :app:desktopTest` — preset form state, form dialog and API client suites were run green. - `./gradlew :common:desktopTest` — preset request serialization and built-in tool catalog suites were run green. ## Risks and follow-up - The preference `PUT` is the only intentionally breaking part of this change; clients that stored null auxiliary ids with `enabled` omitted or `true` must now set `enabled: false` or provide ids. - The two new columns are additive with defaults, so pre-existing rows need no data update; downgrade is not supported.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Torvian.eu/chatbot!338
No description provided.