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:
amosblomqvist
2026-10-06 00:38:15 +03:00
parent 6dcad55175
commit a2db80b72d
11 changed files with 247 additions and 21 deletions
+18 -1
View File
@@ -183,6 +183,7 @@ export function Chat({
initialMessages,
initialPrompt,
initiallyInFlight,
initiallyCompacting = false,
initialConfigured = false,
kind = "teach",
}: {
@@ -197,6 +198,11 @@ export function Chat({
/** A teacher turn is running server-side but attached to no client
* (navigated away mid-turn, returned while in flight). */
initiallyInFlight?: boolean;
/** A compaction holds the chat's model gate at mount (navigated away
* mid-compaction — the registry is live server state, the
* compaction-changed event already fired). The transcript spinner
* and send gates key on it. */
initiallyCompacting?: boolean;
/** slotsConfigured(server record) — the persistent setup CTA keys on
* it (BYOK, owner 2026-09-22). The server gate stays the truth; this
* only decides whether the pill rides the composer. */
@@ -623,7 +629,7 @@ export function Chat({
// flips it; the send paths, canSend, and the docks gate on it (the
// server's registry mutex is the structural gate; this is the
// affordance). The summary dialog state rides with it.
const [compacting, setCompacting] = useState(false);
const [compacting, setCompacting] = useState(initiallyCompacting);
const [compactionSummary, setCompactionSummary] = useState<CompactionDividerData | null>(null);
const canSend =
(text.trim().length > 0 || dictation.active) && !pending && !activeProblem && !compacting;
@@ -1425,6 +1431,17 @@ export function Chat({
provider-connect gap. turnInFlight: the same state
entered by external discovery (wake) / the poll
fallback. Never collapsible itself. */}
{/* Compaction (owner round 15): the same register as the
thinking placeholder — a visible session-wide state while
the summarizer holds the model gate. Mutually exclusive with
the THINKING placeholder by construction (the gate sits
BEFORE beginTurn; the settle kick runs after endTurn). */}
{compacting && (
<span className="flex items-center gap-2 font-ui text-xs font-medium uppercase tracking-wide text-muted-foreground">
<BrailleSpinner label="Compacting context" />
<Shimmer duration={1}>COMPACTING CONTEXT…</Shimmer>
</span>
)}
{(status === "submitted" || turnInFlight) && (
<span className="flex items-center gap-2 font-ui text-xs font-medium uppercase tracking-wide text-muted-foreground">
<BrailleSpinner label="Thinking" />