pi-prefix-stabilizer
v0.4.0
Published
Keep Pi's system prompt byte-stable for prefix-cache reuse.
Readme
pi-prefix-stabilizer
Keep Pi's provider request prefix byte-stable across turns, reloads, and resumes so a server-side prefix cache can reuse it.
The extension normalizes install paths inside the system prompt, sorts tool-schema
lists by name, then fingerprints the normalized leading system text. It warns when that
text changes during a session because one early changed token invalidates every cached
block after it. System messages later in the conversation, which Pi sends to models with
supportsMidConvoSystemMessages, extend the cached prefix and do not warn.
Drift is measured against the request as sent. Extension-section removals the extension already strips elsewhere never reach a model and cannot invalidate any cached block, so toggling or re-adding them is silent once those removals are recorded -- which is what keeps a background-task-notification cycle from repeatedly re-prefilling; trailing flips after the seeded prefix don't break it either. Only a change that reaches the model and alters the seeded prefix -- a real package/MCP server, rules/AGENTS.md, skill, cwd, or content edit -- emits the one-time re-prefill warning.
On Pi 0.99, a prompt that an extension returns from before_agent_start replaces the
prompt for typed-prompt runs only. Runs started by extension messages, such as
background-task notifications, use Pi's own prompt, so the leading prompt switches and the
cache is lost twice per such run. When the returned prompt holds exactly Pi's own sections
in a different order (pi-permission-system moves the tool list to the end, for example),
the stabilizer drops it. A returned prompt that adds, removes or reorders any line within a
section is left in place.
Pi 1.0 records prompt changes as system-message patches by section name. A run started by
an extension message (a background-task notification, for example) skips
before_agent_start, so the patch pi writes at that run's next turn records every
extension-contributed section as removed (the hook simply did not run), and the next typed
prompt adds the same text back. For models without supportsMidConvoSystemMessages pi folds
the patches into the leading system message, so each flip rewrites the prompt head; for models
with it, each flip adds a "Removed section" and an "Updated section" message. Before each
request the stabilizer leaves out of the request copy:
- removals of extension sections in patches written during a run that no typed prompt
started (removals of pi's own sections, such as
skills, are kept), and - patch entries that set a section to the text it already has, and the patches this leaves empty.
The session transcript is not changed. The stabilizer records each patch it leaves out as a
pi-prefix-stabilizer custom entry when it first leaves it out, so a resume, /reload or
/tree sends the same request. A fork copies only the branch up to the selected message, so
forking from a follow-up that was queued in the same turn as a dropped patch loses that record. A run counts as typed only when its first request ends in
the prompt before_agent_start saw. A
session from before 0.3.0 is sent as pi recorded it. The trade-off: an extension section
genuinely removed during a run that no typed prompt started stays in the request until a
later patch changes that section.
External contract
The provider must offer content-addressed prefix caching whose cache key depends on the rendered request prefix. This extension stabilizes bytes; it does not enable or configure the server cache.
For path normalization, packagePathSuffix must identify the volatile suffix in the
rendered prompt. stablePath is the replacement shown to the model. If tools or the
model need that path to resolve on disk, create it yourself or opt into
createSymlink.
If the contract is unmet
Without provider-side prefix caching, requests remain correct but receive no cache benefit. If the configured package suffix is absent, no path is rewritten; tool sorting and drift detection still run. A genuine normalized-prompt change emits a warning for that change, while identical repeats do not re-warn.
A mention of the package suffix is only ever treated as a volatile install path -- and
so only ever rewritten to stablePath -- if it is absolute and actually exists on
disk. A relative mention or an absolute-looking one that does not exist is left alone:
it was never an install path, so rewriting it would change the text's meaning rather
than stabilise a fact.
Symlink creation is off by default. If explicitly enabled and the link cannot be
created, the failure is written to the configured log when logging is enabled and the
turn continues. It also never replaces an existing symlink with a bad one: if the
candidate install root does not exist on disk, whatever is already at stablePath is
left untouched.
In pi's print (-p) and json modes there is no UI to notify, so the drift warning that
would otherwise be silent there is written to stderr instead, once per process.
Load order
Load pi-prefix-stabilizer before pi-compaction-cache.
The stabilizer must normalize the ordinary provider payload before the compaction extension captures it as the live cache prefix.
List it after any extension whose reordered prompt it should drop. Pi runs
before_agent_start handlers in load order, so the stabilizer only sees prompts returned
by extensions loaded before it.
pi install npm:pi-prefix-stabilizer
pi install npm:pi-compaction-cacheSettings
The three JSON settings resolve from pi-prefix-stabilizer.json in the global Pi
config directory, then from a trusted project's Pi config directory, then from the
environment. Later sources win. Project settings are ignored when the project is
untrusted.
| JSON key | Default | Environment override | Meaning |
|---|---|---|---|
| stablePath | $HOME/.pi/pi-home | PI_PREFIX_STABILIZER_STABLE_PATH | Stable replacement for a volatile package install path. |
| packagePathSuffix | node_modules/@earendil-works/pi-coding-agent | PI_PREFIX_STABILIZER_PACKAGE_PATH_SUFFIX | Suffix used to find the absolute package root in prompt text. |
| createSymlink | false | PI_PREFIX_STABILIZER_CREATE_SYMLINK=1 | Create or update stablePath to point at the discovered package root. |
Two environment-only controls preserve the original operational interface:
| Environment variable | Default | Meaning |
|---|---|---|
| PI_PREFIX_STABILIZER | enabled | Set to 0 to disable the extension. |
| PI_PREFIX_STABILIZER_LOG | unset | Append JSON records for rewrites, drift, and symlink operations. |
Default installation performs no filesystem writes.
Measurements
Measured with a large-context model behind a prefix-caching server:
| request | prompt tokens | TTFT | cache hit | |---|---:|---:|---:| | cold prompt | about 132,000 | 145.33s | 0.00% | | byte-identical repeat | about 132,000 | 0.54s | 99.95% | | first four characters changed | about 132,000 | 144.68s | 0.00% |
The stable repeat was about 270 times faster to first token. In the incident that motivated the package, an install-path change diverged near token 2,649 of a 118,031-token request; 97.8% of the prompt had to be prefilled again, producing an observed 127.5–137.5s delay. At the measured 871-token/s prefill rate, the expected delay was 132.5s.
Reproduce the measurements
- Capture one rendered provider payload and the server's prompt-token, cached-token, and TTFT metrics.
- Send it cold, then repeat it byte-for-byte.
- Change a few characters early in the system prompt and send it again.
- Compare cached-token counts and TTFT across the three requests.
- Enable this extension, repeat the experiment across a package reinstall or other install-path change, and confirm that the normalized payload remains identical.
- Reorder tool discovery without changing the tools and confirm that normalization prevents a drift warning; then make a real system-prompt change and confirm one warning is shown.
Cache capacity and eviction policy remain server concerns; test them separately under the deployment's real concurrency and request mix.
