Repository navigation
pi-plugin: forced system prompt hides later extensions' prompt sections from the provider #649
Description
Problem
On Pi, the pi-plugin's before_agent_start handler injects the Magic Context guidance block by returning { systemPrompt }, i.e. forcing the whole system prompt to opaque text. Per Pi's documented semantics, a forced prompt replaces the entire prompt for the run while the transcript keeps recording structured sections:
Prefer changing prompt sections, selected tools, or guidelines so Pi can append a transcript delta. Returning
systemPrompt, or settingforceSystemPrompt, replaces the whole prompt for that run while the transcript continues recording the structured sections. Providers receive the forced text as their leading system prompt. — Pi extension docs
Mechanics in Pi core (@earendil-works/pi-coding-agent):
emitBeforeAgentStartrunsbefore_agent_starthandlers in extension load order; a handler returningsystemPromptsetscurrentOptions.forceSystemPrompt(dist/core/extensions/runner.js).buildSystemPromptStatethen takes the forced branch —return { content: input.forceSystemPrompt }— an opaque string with no sections (dist/core/system-prompt.js).
Consequence: any extension registered after magic-context that follows Pi's own recommendation and injects via systemPromptOptions.sections has its section diffed into the transcript but never rendered into the provider request. Its system-prompt content silently never reaches the model.
Concrete repro
~/.pi/agent/settings.json → packages:
...
"npm:@cortexkit/pi-magic-context", // position N
...
"<any extension using opts.sections>" // any position > N
Reproduced with a memory-index extension that injects its snapshot with opts.sections in before_agent_start (the documented API):
- Session transcript's system message contains its section (Pi keeps recording it).
- The model's actual context does not — the provider receives magic-context's forced text, rendered before the later handler ran. The model cannot see the section at all.
- Moving that extension before magic-context in
packagesmakes the section appear — load-order-dependent breakage of the host contract, not a bug in the affected extension.
Any third-party extension using the recommended sections API and loading after magic-context breaks the same way.
Proposed design
Inject the guidance block through the structured sections API instead of forcing:
event.systemPromptOptions.sections.magic_context = block;
// no `return { systemPrompt }`Why forcing is unnecessary on Pi: the forced return is only load-bearing for processSystemPromptForCache's date freezing, which rewrites prompt text sections cannot express. But the Today's date: line it freezes is an OpenCode artifact (packages/plugin/src/hooks/magic-context/system-prompt-hash.ts: "OpenCode 2 stores instruction updates"); Pi's system prompt has no such line, so on Pi the frozen prompt always equals the composed prompt and forcing buys nothing.
Cache stability: unchanged or better. The block is byte-stable within a session, so Pi's section diffing produces no transcript delta and the provider-visible prefix stays identical; when content genuinely changes, Pi appends a mid-transcript system delta instead of replacing the leading prompt.
Hash bookkeeping: untouched — processSystemPromptForCache still runs on the composed reference prompt; hashChanged → refresh signals work as before.
Scope: pi-plugin only. The OpenCode plugin (packages/plugin) has its own injection path and is not affected by this change.
Implementation ready in PR #648.
Activity
magic-alfonso commentedon Oct 10, 2026
Thanks for the detailed report and the PR. The load-order effect is plausible: when the handler returns
systemPrompt, Pi treats the whole prompt as forced, and a section added by a later extension wouldn't be rendered.Before changing it, I want to check two things on real Pi and Oh My Pi hosts, because this path also keeps the provider's prompt cache stable:
Today's date:line; if Pi's prompt has no matching line, that part is inert there);systemPromptOptions.sectionsdoes to the bytes sent for existing sessions, both after an upgrade and when the Magic Context block changes mid-session.I'll follow up here with what we find.
st0nie commentedon Oct 10, 2026
Thanks for looking at it. I can supply evidence for both checks — I ran them against the installed Pi host while preparing the PR.
1. Date line in the host prompt
Pi 1.1.0: no date line exists.
grep -r "Today's date" dist/over the entire installed@earendil-works/pi-coding-agentdistribution returns zero files (also checkedCurrent date). Across both codebases, the only occurrence of that string anywhere is our ownDATE_PATTERN— the freeze branch targets OpenCode's prompt shape and is inert on Pi:liveDateis always null, soresult.systemPrompt === composedPrompton every turn, and the forced return never rewrites a single byte. (Source comment inpackages/plugin/src/hooks/magic-context/system-prompt-hash.tsconfirms the origin: "OpenCode 2 stores instruction updates ("Today's date is now: ...")".)I don't have an Oh My Pi host locally, so I can't grep that one — if OMP's prompt does carry a matching line, note that forcing there still breaks later extensions' sections on every date-drift turn, so a host-side date knob would be the better long-term answer regardless.
2. Bytes sent when switching to sections
Tested with a script that drives Pi 1.1.0's own prompt machinery (
buildSystemPromptSections,buildSystemPromptState,diffSystemPromptSectionsfromdist/core/system-prompt.js,getSystemMessageTextfrompi-ai) — no mocks, the exact functions Pi calls at runtime.later_extbelow stands for any extension registered after magic-context that injects viaopts.sections:Reading per scenario:
undefined, the provider-visible prefix stays identical — exactly the cache behaviour the forced path was protecting (D).magic_contextkey; the leading system prompt is not re-sent (C). Under the forced path this same event replaces the entire leading prompt — total prefix invalidation. Sections are strictly better here.+magic_context) and previously-hidden sections become visible (E). The unavoidable one-time change is the MC block moving from the forced leading-prompt suffix into a structured section — inherent to any fix for this bug, and after it the prefix is stable going forward.Tests on the PR cover both review findings: the forced-prompt ban is a regex (
/return\s*\{\s*systemPrompt\b/), and a behavioural test dispatchesbefore_agent_startthrough the real extension with a second later-registered handler writing its own section, asserting both sections coexist in the sharedsystemPromptOptions.sectionsmap.st0nie commentedon Oct 10, 2026
any idea?
magic-alfonso commentedon Oct 10, 2026
Thanks, that matches most of what we found. We ran the checks against real hosts with a mock provider that captures each request body: Pi 0.87.1 and 1.1.0, and Oh My Pi 18.2.6 and 18.8.7.
Today's date:line our cache code freezes, so dropping the forced prompt costs nothing there.before_agent_starthas noevent.systemPromptOptions, so the PR's assignment throws inside our handler. Our handler catches the error, which means Oh My Pi requests would go out with no Magic Context guidance at all. Oh My Pi needs to keep a prompt-return path.systemfield was rewritten when the Magic Context block changed, and stayed stable after that. Pi records only amagic_contextdelta in the transcript, but the bytes sent to the provider still changed at the start of the request. So we can't claim that part avoids a prefix change, at least on that provider.We'll follow up with the approach for the fix.
fix(pi-plugin): inject system prompt via sections API when the host h…
st0nie commentedon Oct 10, 2026
Thanks for running the full host matrix — that's a much stronger verification than my single-host checks. Replies per finding:
Bug confirmed on Pi 0.87.1 / 1.1.0; no date line on either host — matches what I found. Good to have the OMP date-line data point too.
Oh My Pi regression — owned, and fixed. You're absolutely right: my PR assigned
event.systemPromptOptions.sections.magic_contextunconditionally, which throws on OMP (nosystemPromptOptions), and our owncatchwould then turn that into requests going out with no guidance block at all — strictly worse than the status quo there. I've updated the PR with capability-detected injection:Pi gets composable sections; OMP keeps the forced return as its only injection path. The updated tests cover both host shapes through the real extension: a Pi-shaped event (asserting a later-registered extension's section coexists with ours in the shared map) and an OMP-shaped event without
systemPromptOptions(asserting the handler doesn't throw and still returns a forced prompt carrying the block). A source-contract test additionally requires every forced return in the handler to sit on the no-sections branch, so a future refactor can't quietly reintroduce either failure mode.Mid-session block change — conceded, my "strictly better" claim was wrong. Your captured Anthropic-path bodies show the provider's leading system field gets rewritten when the block changes even with sections (Pi collapses the transcript delta into the request head). The accurate claim is no worse than the forced path there — forced also rewrites the whole leading prompt on every block change — with unchanged turns byte-stable under both. I've corrected the PR description.
Upgrade cost — agreed: one-time leading-prompt change for existing sessions, byte-stable afterwards.
The PR (branch
st0nie:fix/pi-system-prompt-section-injection, single commit) is updated with all of the above. Happy to reshape it further once you share the approach you have in mind — or feel free to take the design in whatever direction fits; the issue has the full mechanics either way.fix(pi-plugin): inject system prompt via sections API when the host h…
fix(pi-plugin): inject system prompt via sections API when the host h…
magic-alfonso commentedon Oct 10, 2026
Thanks again for the fix in #648. It is merged on master and will ship in the next release.
We checked it on real hosts with a mock provider capturing each request:
mason: draft v0.48.0 release notes
magic-alfonso commentedon Oct 12, 2026
Fixed in 0.48.0 via PR #648: on Pi, Magic Context adds its guidance as a prompt section instead of replacing the whole system prompt, so sections from extensions that load later reach the model. Oh My Pi keeps the previous path. Thanks @st0nie.