Back to blog
Your coding agent can't see its context meter. So we gave it one.

Long coding sessions have a small asymmetry: the person at the keyboard can see Pi's context meter, but the model doing the work cannot.
The model understands context windows in general. Pi also tells it after conversation history has been compacted. What it does not receive is the live fact that the current session has crossed 80% or 90% of the active model's context window.
We built a tiny extension with Pi's Extension API to expose exactly that—and nothing more.
The missing signal
A coding agent can spend a long time inside one run: reading files, executing commands, following references, and making edits across many model calls. Context can move from comfortable to constrained while the agent is still head-down in that loop.
The user sees the meter move. The model does not.
Context Sentinel adds two messages:
1[Context sentinel] Context window usage crossed 80%. Approximately 41k tokens remain. 2[Context sentinel] Context window usage crossed 90%. Approximately 20k tokens remain.
The remaining-token estimate is computed from Pi's usage snapshot and rounded to the nearest thousand. If token usage is temporarily unavailable, the message safely falls back to the threshold-only line.
Both the user and the model see them. There is no recommendation attached.
Why the message says so little
Our first version tried to be helpful:
1Reduce optional exploration, finish atomic steps, make decisions explicit, 2and reread source files before relying on older details.
That was too much policy hidden inside telemetry. Depending on the task, it could make the agent stop exploring too early, reread files unnecessarily, or optimize for closure when the correct move was to keep investigating.
We also considered warning that older details might be unreliable. That belongs after lossy summarization, not merely at high context usage—and Pi already marks compaction explicitly.
The extension's job is therefore narrow: provide a fact the model does not otherwise have. The model can account for that fact alongside the actual task, its instructions, and the state of the work.
Delivery matters more than the counter
The first implementation checked usage only after the agent had fully settled. It avoided interrupting a run, but it missed the point. If context crosses a threshold halfway through a tool-heavy task, awareness is most useful before the next model call—not after all the work is finished.
The final extension uses two delivery paths:
- During a tool-driven run,
turn_endchecks usage. If a threshold was crossed, the message is delivered assteerbefore the next model call that was already going to happen. - After a final response,
agent_settledperforms the same check. The message is appended while idle and joins the user's next turn, without creating an extra model turn.
This distinction keeps the signal timely without manufacturing work just to announce it.
Working with Pi, not replacing it
Context Sentinel does not compact, summarize, prune, or rewrite context. Pi already owns those mechanisms.
The extension only:
- reads live usage with
ctx.getContextUsage(); - announces thresholds at 80% and 90%;
- shows each threshold once per compaction cycle;
- records the announced tier with Pi's context-free
appendEntrystate mechanism; - restores state from the active session branch after reload or resume;
- keeps a message-details fallback for sessions created before dedicated state entries;
- resets its threshold cycle after Pi compacts.
State entries never participate in model context, so persistence adds no conversation tokens. The extension remains deliberately small: it is instrumentation, not a second context-management system.
Install it
Download context-sentinel.ts and place it in either:
1~/.pi/agent/extensions/context-sentinel.ts
or, for one project:
1.pi/extensions/context-sentinel.ts
Then start Pi or run /reload.
Download the Context Sentinel source code
Full source
1import type { 2 ExtensionAPI, 3 ExtensionContext, 4} from "@earendil-works/pi-coding-agent"; 5 6const MESSAGE_TYPE = "context-sentinel"; 7const STATE_TYPE = "context-sentinel-state"; 8 9interface SentinelDetails { 10 kind: "threshold"; 11 tier?: number; 12 percent?: number; 13 remainingTokens?: number; 14} 15 16interface SentinelState { 17 highestAnnouncedTier: number; 18} 19 20const THRESHOLDS = [80, 90] as const; 21 22interface UsageSnapshot { 23 percent: number; 24 remainingTokens?: number; 25} 26 27function getUsage(ctx: ExtensionContext): UsageSnapshot | undefined { 28 const usage = ctx.getContextUsage(); 29 if (!usage || usage.percent === null || usage.percent === undefined) 30 return undefined; 31 return { 32 percent: usage.percent, 33 remainingTokens: 34 usage.tokens === null 35 ? undefined 36 : Math.max(0, usage.contextWindow - usage.tokens), 37 }; 38} 39 40function formatTokens(count: number): string { 41 return count >= 1000 ? `${Math.round(count / 1000)}k` : `${count}`; 42} 43 44function isSentinelDetails(value: unknown): value is SentinelDetails { 45 if (!value || typeof value !== "object") return false; 46 const details = value as Partial<SentinelDetails>; 47 return details.kind === "threshold"; 48} 49 50function isSentinelState(value: unknown): value is SentinelState { 51 if (!value || typeof value !== "object") return false; 52 return ( 53 typeof (value as Partial<SentinelState>).highestAnnouncedTier === "number" 54 ); 55} 56 57export default function contextSentinel(pi: ExtensionAPI) { 58 let highestAnnouncedTier = 0; 59 60 const announceCrossedTier = ( 61 ctx: ExtensionContext, 62 deliverAs?: "steer", 63 ): void => { 64 const usage = getUsage(ctx); 65 if (!usage) return; 66 67 const threshold = [...THRESHOLDS] 68 .reverse() 69 .find( 70 (candidate) => 71 usage.percent >= candidate && candidate > highestAnnouncedTier, 72 ); 73 if (!threshold) return; 74 75 highestAnnouncedTier = threshold; 76 const remaining = 77 usage.remainingTokens === undefined 78 ? "" 79 : ` Approximately ${formatTokens(usage.remainingTokens)} tokens remain.`; 80 pi.sendMessage( 81 { 82 customType: MESSAGE_TYPE, 83 content: `[Context sentinel] Context window usage crossed ${threshold}%.${remaining}`, 84 display: true, 85 details: { 86 kind: "threshold", 87 tier: threshold, 88 percent: usage.percent, 89 remainingTokens: usage.remainingTokens, 90 } satisfies SentinelDetails, 91 }, 92 deliverAs ? { deliverAs } : undefined, 93 ); 94 pi.appendEntry(STATE_TYPE, { 95 highestAnnouncedTier, 96 } satisfies SentinelState); 97 }; 98 99 pi.on("session_start", (_event, ctx) => { 100 highestAnnouncedTier = 0; 101 102 // Restore state from the active branch only. A compaction starts a fresh 103 // pressure cycle because context usage drops after the summary is created. 104 // State lives in custom entries (appendEntry); sentinel messages from 105 // sessions created before the state entry existed still count as fallback. 106 const branch = ctx.sessionManager.getBranch(); 107 for (let index = branch.length - 1; index >= 0; index--) { 108 const entry = branch[index]; 109 if (entry.type === "compaction") break; 110 if ( 111 entry.type === "custom" && 112 entry.customType === STATE_TYPE && 113 isSentinelState(entry.data) 114 ) { 115 highestAnnouncedTier = Math.max( 116 highestAnnouncedTier, 117 entry.data.highestAnnouncedTier, 118 ); 119 continue; 120 } 121 if (entry.type !== "message" || entry.message.role !== "custom") continue; 122 if ( 123 entry.message.customType !== MESSAGE_TYPE || 124 !isSentinelDetails(entry.message.details) 125 ) 126 continue; 127 128 highestAnnouncedTier = Math.max( 129 highestAnnouncedTier, 130 entry.message.details.tier ?? 0, 131 ); 132 } 133 134 // On resume/reload, surface any threshold already crossed before the user 135 // submits another message. 136 announceCrossedTier(ctx); 137 }); 138 139 pi.on("turn_end", (event, ctx) => { 140 if (event.toolResults.length === 0) return; 141 142 // A tool-driven run already has another model call coming. Steer that 143 // existing continuation as soon as context pressure crosses a tier. 144 announceCrossedTier(ctx, "steer"); 145 }); 146 147 pi.on("agent_settled", (_event, ctx) => { 148 // If a final response crossed the tier without another tool continuation, 149 // append the warning while idle. It is visible now and reaches the model 150 // with the user's next message, without manufacturing an extra turn. 151 announceCrossedTier(ctx); 152 }); 153 154 pi.on("session_compact", () => { 155 // Pi owns compaction behavior. Sentinel only starts a fresh threshold cycle. 156 highestAnnouncedTier = 0; 157 }); 158}
Awareness without instructions
The useful part of this extension is not the percentage itself. It is the boundary around the feature.
A context meter is telemetry. Once telemetry starts telling the agent how to work, it becomes another instruction layer—one that may conflict with the task at exactly the moment the context is most crowded.
Expose the missing fact. Let the agent reason from it.