# FE handoff — conversation compacting Courier this to `../dispatch-web`. All changes are ADDITIVE. ## What shipped (backend) Conversation compaction: summarize old history into a summary + recent N, preserving the full pre-compaction history in a separate archive conversation. Creates a linked chain of archives you can walk backward. Two modes: - **Manual**: `POST /conversations/:id/compact` — triggers immediately. - **Automatic**: after each turn settles, the backend checks if the last turn's input tokens exceeded the per-conversation `compactThreshold` (default 85). If so, compaction runs automatically (fire-and-forget, non-blocking). ## How compaction works — non-destructive, chained The compacted conversation **keeps its original ID** (so messaging between agents still works). The old full history is **forked** to a new archive conversation (new UUID). The archive inherits the source's `compactedFrom`, creating a chain: ``` Compaction 1: A (ID "abc") — full history forked to X (new ID). A's history replaced with [summary + recent N]. A.compactedFrom = X Compaction 2: A (ID "abc") — current history forked to Y (new ID). A's history replaced with [new summary + recent N]. A.compactedFrom = Y Y.compactedFrom = X (inherited from A's pre-compaction state) Chain: A → Y → X (walk compactedFrom backward) ``` Each archive is an **immutable snapshot** — a complete copy of the conversation at the time of that compaction. History is never destroyed. The FE **does not switch tabs** — the conversation ID doesn't change. Just reload the history. ## Bump pinned deps - `@dispatch/wire` → `0.11.0` - `@dispatch/transport-contract` → `0.15.0` ## New types ```ts // @dispatch/wire — ConversationMeta now has compactedFrom export interface ConversationMeta { readonly id: string; readonly createdAt: number; readonly lastActivityAt: number; readonly title: string; readonly status: ConversationStatus; // "active" | "idle" | "closed" /** Points to the archive conversation with full pre-compaction history. */ readonly compactedFrom?: string; } // @dispatch/wire export interface CompactionResult { readonly summary: string; readonly newConversationId: string; // ID of the archive (old full history) readonly messagesSummarized: number; readonly messagesKept: number; } // @dispatch/transport-contract — WS message (server → client) export interface ConversationCompactedMessage { readonly type: "conversation.compacted"; readonly conversationId: string; // the conversation (ID unchanged) readonly newConversationId: string; // the archive ID (old full history) readonly messagesSummarized: number; readonly messagesKept: number; } // Added to WsServerMessage union. // @dispatch/transport-contract — HTTP response types export interface CompactResponse { readonly conversationId: string; // the conversation (ID unchanged) readonly newConversationId: string; // the archive ID (old full history) readonly messagesSummarized: number; readonly messagesKept: number; } export interface CompactPercentResponse { readonly conversationId: string; readonly percent: number; // 0 = manual only; null = default 85 } export interface SetCompactPercentRequest { readonly percent: number; } ``` ## `POST /conversations/:id/compact` — manual compaction Triggers compaction on demand. Optional JSON body: ```json { "keepLastN": 10, "modelName": "umans/umans-glm-5.2" } ``` - `keepLastN` (default 10): how many recent messages to retain. - `modelName`: override the model used for summarization. 200 response: `CompactResponse` — includes `newConversationId` (the archive ID). The conversation ID in the response is the same as the request — the ID doesn't change. The FE should reload the conversation history. 409: `{ error: string }` — conversation is generating, too short, percent not exceeded, etc. 503: compaction service not available. ## `GET /conversations/:id/compact-percent` — read percent 200: `CompactPercentResponse { conversationId, percent }` - `percent: 0` — auto-compact explicitly disabled (manual only). - `percent: null` (not stored) — **default: 85** (85% tokens). The FE should display 85 as the default value in the settings UI. - Any positive number — auto-compact triggers when the last turn's input tokens exceed this value. ## `PUT /conversations/:id/compact-percent` — set percent Body: `SetCompactPercentRequest { percent: number }` - `0` explicitly disables auto-compact. - Any positive number sets the trigger percent. - To "reset to default", set it to 85. ## `conversation.compacted` WS message Broadcast to all connected WS clients when compaction completes. The FE should **reload the conversation history** via `GET /conversations/:id` (the conversation ID hasn't changed — just reload the same ID). The first message will now be a system summary. No tab switching needed — the ID is the same. ## What the FE needs to do 1. **Compact button** in the conversation toolbar → `POST /conversations/:id/compact`. Show a loading indicator while waiting. On success, reload the conversation history (same ID — just re-fetch). 2. **Settings UI** for compact percent: `PUT /conversations/:id/compact-percent` with `{ percent: number }`. A number input (0 = manual only, default 85). Read the current value via `GET /conversations/:id/compact-percent`. 3. **Handle `conversation.compacted` WS messages**: reload the conversation history via `GET /conversations/:id` (same ID, no tab switch). 4. **"View predecessor" link**: when `ConversationMeta.compactedFrom` is present, show a link that opens the archive conversation in a read-only view (or a new tab). Load it via `GET /conversations/:compactedFrom`. The archive has `status: "closed"` and title `"Archive: "`. Each archive may also have its own `compactedFrom` — walk the chain backward to see every snapshot. 5. **Archives in conversation list**: archives appear in `GET /conversations?status=closed`. They have `compactedFrom` chaining to the previous archive (if any). The FE can show them in a history view. 6. **Visual indicator**: show a badge on conversations that have a `compactedFrom` (they've been compacted). E.g. "Compacted" badge or chain icon. ## CLI `dispatch compact ` — triggers manual compaction. Resolves short IDs like other commands. The response includes the archive ID.