v1.11: opencode-style compaction port

- compaction.ts: usable/isOverflow/estimate/turns/select/buildPrompt/process
- compaction-prompt.ts: SUMMARY_TEMPLATE verbatim from opencode
- schema: messages.{compacted_at,summary,tail_start_id} + chats.needs_compaction
- inference: auto-trigger on overflow, pre-fetch compaction before next turn
- /compact slash command rewired to new path
- WS: chat_status working/idle around compaction + compacted frame
- frontend: SummaryCard + sonner toast on compacted
- 24 unit tests for pure functions
This commit is contained in:
2026-05-20 19:05:35 +00:00
parent 6aab4f7d2a
commit dc43dd44f9
14 changed files with 1063 additions and 113 deletions

View File

@@ -1,5 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
import type { Message, WsFrame } from '@/api/types';
import { api } from '@/api/client';
import { sessionEvents } from './sessionEvents';
// session_renamed frame removed from WsFrame — it was declared but never
@@ -161,6 +163,12 @@ function applyFrame(state: State, frame: WsFrame): State {
: state.messages;
return { ...state, messages: next, error: frame.error };
}
case 'compacted': {
// v1.11: side effects (refetch + toast) live in ws.onmessage; the
// reducer just no-ops so TS exhaustiveness is satisfied without
// duplicating async work inside a synchronous reducer.
return state;
}
}
}
@@ -196,6 +204,25 @@ export function useSessionStream(sessionId: string | undefined) {
ws.onmessage = (ev) => {
try {
const frame = JSON.parse(typeof ev.data === 'string' ? ev.data : '') as WsFrame;
// v1.11: on a compaction completion, re-fetch the message list so
// the new summary row + the cohort of compacted_at-stamped older
// rows render correctly. We dispatch the fresh list as a synthetic
// 'snapshot' frame so the reducer's existing path handles state
// replacement (no need for a parallel "refetched" path).
// The toast is purely UX feedback; missing it would still leave
// the chat in a valid state.
if (frame.type === 'compacted') {
toast.success('Context compacted to free space');
void api.messages
.list(frame.session_id)
.then((messages) => {
setState((s) => applyFrame(s, { type: 'snapshot', messages }));
})
.catch((err: unknown) => {
console.warn('compacted refetch failed', err);
});
return;
}
setState((s) => applyFrame(s, frame));
} catch (err) {
console.warn('bad ws frame', err);