@@ -22,42 +22,63 @@ export interface ContextBreakdown {
2222 free : number ;
2323}
2424
25+ /**
26+ * This session's fixed context overhead, estimated worker-side from the real system prompt + tool
27+ * definitions (see `ContextCost` / the `session.context.cost` event). Optional: until the worker
28+ * has reported it we fall back to the floor constants below.
29+ */
30+ export interface ContextOverhead {
31+ systemPrompt ?: number ;
32+ builtinTools ?: number ;
33+ mcp ?: number ;
34+ }
35+
2536const DEFAULT_CONTEXT_WINDOW = 200_000 ;
26- const SYSTEM_PROMPT_FLOOR = 1_500 ; // ballpark — pi's base prompt + envelopes.
27- const BUILTIN_TOOLS_FLOOR = 4_500 ; // ballpark — pi's built-in tool definitions (excludes MCP).
37+ // Fallbacks used only until the worker reports the real per-session overhead. Ballparks: pi's base
38+ // prompt + envelopes, and its built-in tool definitions (excludes MCP).
39+ const SYSTEM_PROMPT_FLOOR = 1_500 ;
40+ const BUILTIN_TOOLS_FLOOR = 4_500 ;
2841
2942/**
30- * Derive a per-category breakdown from `ContextUsage` (aggregate, from pi), the visible messages
31- * (used to estimate the messages bucket), and the worker's MCP-tools estimate. When pi hasn't
32- * reported usage yet (`ctx` is undefined) we still surface a floor estimate — including the MCP
33- * figure when known — so the Context tab paints something useful before the first turn.
43+ * Derive a per-category breakdown from `ContextUsage` (aggregate, from pi), the visible messages,
44+ * and the worker's overhead estimate. pi only reports an aggregate `used`, so the breakdown is an
45+ * *attribution* of that single number — not an independent recount.
3446 *
35- * pi only reports an aggregate `used`, so we carve the buckets out of it in priority order
36- * (messages → system prompt → MCP → built-in tools) and they always sum to `used`. Post-turn the
37- * aggregate already includes MCP (those tools are in the real payload), so carving is correct;
38- * pre-turn we fold the MCP estimate into the floor so the bar reflects it immediately.
47+ * The overhead buckets (system prompt + built-in tools + MCP tools) are fixed for the worker's
48+ * lifetime and known precisely, so they're laid down first; **messages is the residual** — whatever
49+ * of `used` is left once the overhead is accounted for. That's the honest split: `used` is
50+ * dominated by the conversation, and the residual captures history + tool results + attachments the
51+ * visible message text alone can't see. The four buckets always sum to `used`.
52+ *
53+ * Before the first turn (`ctx` undefined) there's no aggregate, so `used` is the overhead plus a
54+ * chars/4 estimate of the visible messages. When the worker hasn't reported overhead yet, the floor
55+ * constants stand in.
3956 */
4057export function computeContextBreakdown (
4158 ctx : ContextUsage | undefined ,
4259 messages : ReadonlyArray < { text ?: string } > ,
43- mcpTokens = 0 ,
60+ overhead : ContextOverhead = { } ,
4461) : ContextBreakdown {
45- const messagesTokens = estimateMessagesTokens ( messages ) ;
4662 const contextWindow = ctx ?. contextWindow ?? DEFAULT_CONTEXT_WINDOW ;
47- const mcp = Math . max ( 0 , mcpTokens ) ;
63+ const systemFixed = Math . max ( 0 , overhead . systemPrompt ?? SYSTEM_PROMPT_FLOOR ) ;
64+ const toolsFixed = Math . max ( 0 , overhead . builtinTools ?? BUILTIN_TOOLS_FLOOR ) ;
65+ const mcpFixed = Math . max ( 0 , overhead . mcp ?? 0 ) ;
66+
67+ const messagesTokens = estimateMessagesTokens ( messages ) ;
4868 const used =
4969 typeof ctx ?. tokens === "number" && ctx . tokens > 0
5070 ? ctx . tokens
51- : messagesTokens + SYSTEM_PROMPT_FLOOR + BUILTIN_TOOLS_FLOOR + mcp ;
71+ : systemFixed + toolsFixed + mcpFixed + messagesTokens ;
5272
53- const messagesBucket = Math . min ( messagesTokens , used ) ;
54- const afterMessages = Math . max ( 0 , used - messagesBucket ) ;
55- const systemPrompt = Math . min ( SYSTEM_PROMPT_FLOOR , afterMessages ) ;
56- const afterSystem = Math . max ( 0 , afterMessages - systemPrompt ) ;
57- // MCP claims its estimate next, clamped so it never exceeds the real aggregate; built-in tools
58- // take whatever remains.
59- const mcpBucket = Math . min ( mcp , afterSystem ) ;
60- const tools = Math . max ( 0 , afterSystem - mcpBucket ) ;
73+ // Lay down the fixed overhead in priority order, each clamped to what's left of `used` (so a
74+ // small real aggregate can't push the buckets past it), then hand the remainder to messages.
75+ const systemPrompt = Math . min ( systemFixed , used ) ;
76+ let remaining = used - systemPrompt ;
77+ const tools = Math . min ( toolsFixed , remaining ) ;
78+ remaining -= tools ;
79+ const mcp = Math . min ( mcpFixed , remaining ) ;
80+ remaining -= mcp ;
81+ const messagesBucket = remaining ; // residual — the conversation fills whatever overhead doesn't.
6182 const free = Math . max ( 0 , contextWindow - used ) ;
6283
6384 return {
@@ -66,7 +87,7 @@ export function computeContextBreakdown(
6687 messages : messagesBucket ,
6788 systemPrompt,
6889 tools,
69- mcp : mcpBucket ,
90+ mcp,
7091 free,
7192 } ;
7293}
0 commit comments