Overview
Implement AI-powered message squashing to replace simple concatenation with semantic compression. The infrastructure (SquashOperation entity, repository, database schema) is already complete and waiting for integration.
Current State
What exists:
- ✅
SquashOperation domain model with provenance tracking
- ✅
SquashOperationRepository interface + Exposed implementation
- ✅ Database schema (
squash_operations table)
- ✅
ConversationEngineService with Spring AI ChatModel integration
- ✅ UI with message selection and squash button
What's broken:
- ❌
ConversationService.squashMessages() uses deprecated originalIds instead of squashOperationId
- ❌
TabViewModel.squashSelectedMessages() does simple concatenation (joinToString("\n\n"))
- ❌ No AI semantic compression - just string joining
- ❌ No
SquashOperation records created - audit trail missing
Architecture Design
5-Layer Architecture
Layer 1: Domain Model (✅ Complete)
```kotlin
data class SquashOperation(
val id: Id,
val conversationId: Conversation.Id,
val sourceMessageIds: List<Message.Id>,
val resultMessageId: Message.Id,
val prompt: String? = null, // AI prompt used (null for manual)
val model: String? = null, // "sonnet" (null for manual)
val performedByAgent: Boolean, // true=AI, false=manual
val createdAt: Instant,
)
```
Layer 2: Repository (✅ Complete)
SquashOperationRepository with save/find methods
- Already wired in DI config
Layer 3: AI Engine Service (🔄 Add method)
```kotlin
// ConversationEngineService.kt
suspend fun squashMessages(
conversationId: Conversation.Id,
messageIds: List<Message.Id>,
customPrompt: String? = null
): SquashResult {
// 1. Load messages
// 2. Build AI squash prompt (XML format)
// 3. Call ChatModel with no tools, low temperature
// 4. Return SquashResult(squashedText, prompt, model)
}
data class SquashResult(
val squashedText: String,
val prompt: String,
val model: String
)
```
Layer 4: Conversation Service (🔄 Update existing + add new method)
```kotlin
// Update existing method
suspend fun squashMessages(
conversationId: Conversation.Id,
messageIds: List<Message.Id>,
squashedContent: List
): Conversation? {
// CREATE SquashOperation with performedByAgent=false
// SET Message.squashOperationId (NOT originalIds)
// ... rest of existing logic
}
// New method for AI squash
suspend fun squashMessagesWithAI(
conversationId: Conversation.Id,
messageIds: List<Message.Id>,
squashedText: String,
prompt: String,
model: String
): Conversation? {
// CREATE SquashOperation with performedByAgent=true
// SAVE prompt + model for reproducibility
// ... same Thread creation logic
}
```
Layer 5: UI (🔄 Add AI option with loading state)
- Replace single "Squash" button with dropdown menu
- Options: "Manual (Concatenate)" and "AI Squash"
- Add loading indicator during AI call
- Add
isSquashing state to TabViewModel.UIState
Prompt Engineering Strategy
XML-Structured Prompt for Claude
```xml
Compress the following messages into a single, concise message that preserves essential meaning and intent.
- Preserve all key information and intent from the original messages
- Remove redundancy, verbosity, and unnecessary elaboration
- Maintain the original semantic meaning - do not add interpretations
- Use clear, direct language appropriate for technical communication
- For tool calls and results: summarize the outcome (what was achieved), not the mechanism
- Keep technical details only if they are essential to understanding
- If messages contain errors or corrections, include only the final corrected state
- Output ONLY the compressed message text - no preamble, no meta-commentary
<messages_to_squash>
Original message 1 text...
Original message 2 text...
</messages_to_squash>
```
Content Type Handling
| Content Type |
Extraction Strategy |
Example Output |
| `UserMessage` |
Direct text |
"Find all TypeScript files" |
| `AssistantMessage` |
`structured.fullText` |
"I found 42 TypeScript files..." |
| `ToolCall` |
Summarize intent |
"Searched for TypeScript files" (not raw JSON) |
| `ToolResult` |
Summarize outcome |
"Found 42 matches" (not full output) |
| `Thinking` |
Include reasoning |
"Reasoning: Need to check both src/ and test/" |
| `System` |
Context info |
"Warning: Large file ignored" |
Model Configuration
```kotlin
ClaudeCodeChatOptions.builder()
.model("sonnet") // Current default
.maxTokens(4096) // Enough for squashed result
.temperature(0.3) // Low temp for consistency
.thinkingBudgetTokens(0) // No extended thinking needed
.toolNames(emptySet()) // No tools for squash
.build()
```
Why temperature 0.3?
- Deterministic compression (same input → similar output)
- Consistent quality
- Still allows natural language flow
Implementation Phases
Phase 1: SquashOperation Infrastructure Integration (1-2h)
Goal: Wire existing infrastructure without AI.
Tasks:
Success Metrics:
- ✅ Every manual squash creates `SquashOperation` row in database
- ✅ `Message.squashOperationId` correctly references operation
- ✅ No regression - existing squash flow works identically
Risk: Low - just wiring existing components
Phase 2: AI Squash Engine (2-3h)
Goal: Implement AI semantic compression.
Tasks:
Success Metrics:
- ✅ AI call completes successfully
- ✅ Squashed text is shorter than concatenated original
- ✅ Key information preserved (manual review of 5+ test cases)
- ✅ `SquashOperation` saved with non-null prompt + model
Risk: Medium - prompt quality affects results
Phase 3: UI Integration (2-3h)
Goal: Add AI squash option with loading states.
Tasks:
Success Metrics:
- ✅ User can choose between Manual and AI squash
- ✅ Loading indicator appears during AI call (2-5 seconds)
- ✅ Error messages shown to user if AI fails
- ✅ UI remains responsive during squash
Risk: Low - straightforward UI changes
Phase 4: Custom Prompts (Optional Enhancement) (1-2h)
Goal: Allow user-customized squash behavior.
Tasks:
Use Cases:
- "Squash into bullet points"
- "Keep only technical details, remove explanations"
- "Translate result to Russian"
Success Metrics:
- ✅ Custom instructions visibly affect squash result
- ✅ Custom prompt stored in `SquashOperation` for audit trail
Risk: Low - optional feature, doesn't block MVP
Phase 5: Cleanup & Deprecation (Later) (1-2h)
Goal: Remove deprecated `originalIds` field after stabilization.
Tasks:
Success Metrics:
- ✅ No code references `originalIds`
- ✅ All squash operations tracked via `SquashOperation`
- ✅ Old data either migrated or documented as legacy
Risk: Medium - breaking change requires careful rollout
Effort Estimate
| Phase |
Time |
Complexity |
Priority |
| Phase 1 |
1-2h |
Low |
High |
| Phase 2 |
2-3h |
Medium |
High |
| Phase 3 |
2-3h |
Low |
High |
| Phase 4 |
1-2h |
Low |
Medium (optional) |
| Phase 5 |
1-2h |
Medium |
Low (later) |
| Total MVP (1-3) |
5-8h |
|
|
| Full Implementation |
7-12h |
|
|
MVP = Phases 1-3: Working AI squash with provenance tracking.
Edge Cases & Error Handling
Edge Case: Non-contiguous messages
User selects messages [1, 3, 5] (skipping 2, 4).
Handling:
- Sort by original position before building prompt
- AI sees messages in conversation order
- Result replaces first selected message position
Edge Case: Very long messages
10 messages × 5K tokens = 50K tokens input.
Handling:
- Phase 1: No limit, let Claude CLI handle context window
- If exceeds context: Clear error message to user
- Future: Batch squash or reject upfront with size estimate
Edge Case: Tool calls only
Messages contain only `ToolCall` + `ToolResult`, no user/assistant text.
Handling:
- Extract tool names + summarize results
- Example: "Used grep to find 42 TypeScript files, then read main.ts and found 3 type errors"
Risk: AI produces incoherent result
Squash result loses meaning or introduces errors.
Mitigation:
- Low temperature (0.3) for consistency
- Clear, tested prompt instructions
- User can undo via Thread switching - no data loss
- Manual squash always available as fallback
Risk: Latency
AI call takes 2-5 seconds, user waits.
Mitigation:
- Loading indicator with clear state
- Async operation - UI remains responsive
- User expectation: "AI Squash" label implies processing
Success Criteria
Phase 1 Complete:
- ✅ Manual squash creates `SquashOperation` in database
- ✅ No UI regression
Phase 2 Complete:
- ✅ AI squash engine returns coherent, compressed text
- ✅ Provenance tracking (prompt + model) works
- ✅ 5+ manual test cases pass review
Phase 3 Complete:
- ✅ UI offers Manual vs AI choice in dropdown
- ✅ Loading indicator works
- ✅ Error handling with user feedback
MVP Complete (Phases 1-3):
- ✅ User can AI squash 2+ messages from UI
- ✅ Result appears in conversation correctly
- ✅ Undo works via Thread switching
- ✅ `SquashOperation` audit trail complete for all squashes
Technical Notes
Why Not Streaming for Squash?
Use `chatModel.call()` (blocking) instead of `stream()`:
- Squash result needed in full before saving
- No intermediate UI updates required
- Simpler error handling
- Faster completion (no chunk processing overhead)
Thread Safety
All squash operations are Copy-on-Write:
- New Thread created for result
- Original messages immutable
- Safe for concurrent reads
- Undo via Thread switching
Database Schema (Already Exists)
```sql
CREATE TABLE squash_operations (
id VARCHAR(255) PRIMARY KEY,
conversation_id VARCHAR(255) REFERENCES conversations(id) ON DELETE CASCADE,
source_message_ids TEXT, -- JSON array of message IDs
result_message_id VARCHAR(255) REFERENCES messages(id),
prompt TEXT NULL, -- AI prompt used (null for manual)
model VARCHAR(255) NULL, -- Model name (null for manual)
performed_by_agent BOOLEAN,
created_at TIMESTAMP
);
```
References
- Domain Model Docs: `docs/domain-model.md` (lines 44-69 cover SquashOperation)
- Current Implementation:
- `shared/src/commonMain/kotlin/com/gromozeka/shared/services/ConversationService.kt:231` (manual squash)
- `bot/src/jvmMain/kotlin/com/gromozeka/bot/ui/viewmodel/TabViewModel.kt:377` (UI caller)
- TODO Markers:
- `ConversationService.kt:255` - "TODO: Implement AI-powered squash with SquashOperation tracking"
- `ConversationService.kt:262` - "TODO: migrate to squashOperationId after AI squash implementation"
- `TabViewModel.kt:387` - "TODO: Replace with AI-powered squash using ConversationEngineService"
🤖 Generated with Claude Code
Overview
Implement AI-powered message squashing to replace simple concatenation with semantic compression. The infrastructure (
SquashOperationentity, repository, database schema) is already complete and waiting for integration.Current State
What exists:
SquashOperationdomain model with provenance trackingSquashOperationRepositoryinterface + Exposed implementationsquash_operationstable)ConversationEngineServicewith Spring AI ChatModel integrationWhat's broken:
ConversationService.squashMessages()uses deprecatedoriginalIdsinstead ofsquashOperationIdTabViewModel.squashSelectedMessages()does simple concatenation (joinToString("\n\n"))SquashOperationrecords created - audit trail missingArchitecture Design
5-Layer Architecture
Layer 1: Domain Model (✅ Complete)
```kotlin
data class SquashOperation(
val id: Id,
val conversationId: Conversation.Id,
val sourceMessageIds: List<Message.Id>,
val resultMessageId: Message.Id,
val prompt: String? = null, // AI prompt used (null for manual)
val model: String? = null, // "sonnet" (null for manual)
val performedByAgent: Boolean, // true=AI, false=manual
val createdAt: Instant,
)
```
Layer 2: Repository (✅ Complete)
SquashOperationRepositorywith save/find methodsLayer 3: AI Engine Service (🔄 Add method)
```kotlin
// ConversationEngineService.kt
suspend fun squashMessages(
conversationId: Conversation.Id,
messageIds: List<Message.Id>,
customPrompt: String? = null
): SquashResult {
// 1. Load messages
// 2. Build AI squash prompt (XML format)
// 3. Call ChatModel with no tools, low temperature
// 4. Return SquashResult(squashedText, prompt, model)
}
data class SquashResult(
val squashedText: String,
val prompt: String,
val model: String
)
```
Layer 4: Conversation Service (🔄 Update existing + add new method)
```kotlin
// Update existing method
suspend fun squashMessages(
conversationId: Conversation.Id,
messageIds: List<Message.Id>,
squashedContent: List
): Conversation? {
// CREATE SquashOperation with performedByAgent=false
// SET Message.squashOperationId (NOT originalIds)
// ... rest of existing logic
}
// New method for AI squash
suspend fun squashMessagesWithAI(
conversationId: Conversation.Id,
messageIds: List<Message.Id>,
squashedText: String,
prompt: String,
model: String
): Conversation? {
// CREATE SquashOperation with performedByAgent=true
// SAVE prompt + model for reproducibility
// ... same Thread creation logic
}
```
Layer 5: UI (🔄 Add AI option with loading state)
isSquashingstate to TabViewModel.UIStatePrompt Engineering Strategy
XML-Structured Prompt for Claude
```xml
- Preserve all key information and intent from the original messages - Remove redundancy, verbosity, and unnecessary elaboration - Maintain the original semantic meaning - do not add interpretations - Use clear, direct language appropriate for technical communication - For tool calls and results: summarize the outcome (what was achieved), not the mechanism - Keep technical details only if they are essential to understanding - If messages contain errors or corrections, include only the final corrected state - Output ONLY the compressed message text - no preamble, no meta-commentaryCompress the following messages into a single, concise message that preserves essential meaning and intent.
<messages_to_squash>
Original message 1 text...
Original message 2 text...
</messages_to_squash>
```
Content Type Handling
Model Configuration
```kotlin
ClaudeCodeChatOptions.builder()
.model("sonnet") // Current default
.maxTokens(4096) // Enough for squashed result
.temperature(0.3) // Low temp for consistency
.thinkingBudgetTokens(0) // No extended thinking needed
.toolNames(emptySet()) // No tools for squash
.build()
```
Why temperature 0.3?
Implementation Phases
Phase 1: SquashOperation Infrastructure Integration (1-2h)
Goal: Wire existing infrastructure without AI.
Tasks:
Success Metrics:
Risk: Low - just wiring existing components
Phase 2: AI Squash Engine (2-3h)
Goal: Implement AI semantic compression.
Tasks:
Success Metrics:
Risk: Medium - prompt quality affects results
Phase 3: UI Integration (2-3h)
Goal: Add AI squash option with loading states.
Tasks:
Success Metrics:
Risk: Low - straightforward UI changes
Phase 4: Custom Prompts (Optional Enhancement) (1-2h)
Goal: Allow user-customized squash behavior.
Tasks:
Use Cases:
Success Metrics:
Risk: Low - optional feature, doesn't block MVP
Phase 5: Cleanup & Deprecation (Later) (1-2h)
Goal: Remove deprecated `originalIds` field after stabilization.
Tasks:
Success Metrics:
Risk: Medium - breaking change requires careful rollout
Effort Estimate
MVP = Phases 1-3: Working AI squash with provenance tracking.
Edge Cases & Error Handling
Edge Case: Non-contiguous messages
User selects messages [1, 3, 5] (skipping 2, 4).
Handling:
Edge Case: Very long messages
10 messages × 5K tokens = 50K tokens input.
Handling:
Edge Case: Tool calls only
Messages contain only `ToolCall` + `ToolResult`, no user/assistant text.
Handling:
Risk: AI produces incoherent result
Squash result loses meaning or introduces errors.
Mitigation:
Risk: Latency
AI call takes 2-5 seconds, user waits.
Mitigation:
Success Criteria
Phase 1 Complete:
Phase 2 Complete:
Phase 3 Complete:
MVP Complete (Phases 1-3):
Technical Notes
Why Not Streaming for Squash?
Use `chatModel.call()` (blocking) instead of `stream()`:
Thread Safety
All squash operations are Copy-on-Write:
Database Schema (Already Exists)
```sql
CREATE TABLE squash_operations (
id VARCHAR(255) PRIMARY KEY,
conversation_id VARCHAR(255) REFERENCES conversations(id) ON DELETE CASCADE,
source_message_ids TEXT, -- JSON array of message IDs
result_message_id VARCHAR(255) REFERENCES messages(id),
prompt TEXT NULL, -- AI prompt used (null for manual)
model VARCHAR(255) NULL, -- Model name (null for manual)
performed_by_agent BOOLEAN,
created_at TIMESTAMP
);
```
References
🤖 Generated with Claude Code