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:
+18
-1
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user