compaction: the usage-anchored trigger + visible session state (round 15)
The relational-databases incident, diagnosed and fixed: TRIGGER — pi's estimator law, adopted exactly: anchor on the last assistant message's provider-reported usage (totalTokens || the field sum) and chars/4-estimate ONLY what was appended after it. The anchor scan skips aborted/zero-usage assistants and STOPS at the newest compaction marker (pre-cut usage described context the next request will not carry — pi's distrust rule). The old estimator (JSON.stringify of every message /4) tripled the true context (measured: 84,412 'tokens' estimated vs 38,799 provider-reported on the incident record) — scaffolding, escaped LaTeX, thinking blocks and app-side details all counted. estimateMessageTokens now counts wire text only (text + thinking + toolCall name/args; content text for user/toolResult/custom). The unanchored fallback runs the plain heuristic over the CUT-HONORED projection plus the additive prompt term; the anchored path adds nothing (usage already billed prompt and tools). OBSERVABILITY — compaction is a visible session-wide state (owner spec): the transcript-bottom placeholder register gains COMPACTING CONTEXT… (BrailleSpinner + Shimmer, the THINKING placeholder's twin, mutually exclusive by construction — the gate precedes beginTurn), and the sidebar row shows 'Compacting context · <ago>' with the spinner (sessionMeta outranks busy; compacting-changed joins turn-started/turn-settled as refresh triggers). Liveness rides the API like turn-busy: ChatListItem.compacting + compactionInFlight on both GET /api/chats and GET /api/chats/[id] (initiallyCompacting covers the navigate-back-mid-compaction mount — the event already fired), exported through the server barrel. Also: the relational-databases record rolled back to pre-compaction (marker, post-marker turns, tree reset; backup in /tmp) for a clean retest. The summarizer-usage 'gap' was a misread — steps[] was populated all along.
This commit is contained in:
@@ -243,6 +243,7 @@ export function timeAgo(iso: string, now = new Date()): string {
|
||||
* still rides the API payload, so this is a one-line flip back if it's
|
||||
* ever wanted. */
|
||||
function sessionMeta(chat: ChatListItem): string {
|
||||
if (chat.compacting) return `Compacting context · ${timeAgo(chat.updatedAt)}`;
|
||||
if (chat.pendingQuestion) return `Waiting for your answer · ${timeAgo(chat.updatedAt)}`;
|
||||
if ((chat.kind ?? "teach") === "practice" && chat.pendingProblem) {
|
||||
return `Problem waiting · ${timeAgo(chat.updatedAt)}`;
|
||||
@@ -284,7 +285,7 @@ export function SessionRow({
|
||||
kbdSelected && !active && "bg-accent/60 ring-1 ring-border",
|
||||
)}
|
||||
>
|
||||
<SessionStatus busy={chat.busy} />
|
||||
<SessionStatus busy={chat.busy || chat.compacting} />
|
||||
<span className="min-w-0 flex-1">
|
||||
{/* Titles are the learner's/teacher's WORDS — Source Sans 3, the
|
||||
same voice the words were typed in (every input is lh-prose);
|
||||
@@ -486,6 +487,7 @@ function SessionsSidebar() {
|
||||
useServerEvent("chats-changed", scheduleEventRefresh);
|
||||
useServerEvent("turn-started", scheduleEventRefresh);
|
||||
useServerEvent("turn-settled", scheduleEventRefresh);
|
||||
useServerEvent("compaction-changed", scheduleEventRefresh);
|
||||
useServerResync(() => void refreshRef.current());
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
||||
Reference in New Issue
Block a user