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.
41 lines
1.4 KiB
TypeScript
41 lines
1.4 KiB
TypeScript
import { notFound } from "next/navigation";
|
|
import { Chat } from "@/components/chat";
|
|
import { slotsConfigured } from "@/src/core";
|
|
import { compactionInFlight, getChat, isTurnRunning, readSettingsRecord } from "@/src/server";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function ChatPage({
|
|
params,
|
|
searchParams,
|
|
}: {
|
|
params: Promise<{ id: string }>;
|
|
searchParams: Promise<{ prompt?: string }>;
|
|
}) {
|
|
const [{ id }, { prompt }] = await Promise.all([params, searchParams]);
|
|
const chat = getChat(id);
|
|
if (!chat) notFound();
|
|
// The persistent setup CTA's seed (BYOK, owner 2026-09-22): slotsConfigured
|
|
// over the settings record, server-read — the same gate home/new read. The
|
|
// server's 503 stays the truth; this only decides whether the pill rides.
|
|
const record = readSettingsRecord();
|
|
return (
|
|
<Chat
|
|
key={chat.id}
|
|
chatId={chat.id}
|
|
title={chat.title}
|
|
kind={chat.kind ?? "teach"}
|
|
// Wave D: the record IS native agent history — the client view is
|
|
// derived app-side (core/view.ts projectView; GET serves the same
|
|
// native messages).
|
|
initialMessages={chat.messages}
|
|
initialConfigured={slotsConfigured(record?.models)}
|
|
initialPrompt={prompt}
|
|
// A teacher turn that outlived its client: the remounted view must
|
|
// attach (busy shimmer + poll), never resurrect the stale record.
|
|
initiallyInFlight={isTurnRunning(chat.id)}
|
|
initiallyCompacting={compactionInFlight(chat.id)}
|
|
/>
|
|
);
|
|
}
|