Repository navigation
[pi-plugin] Dispatch fence aborts turns when no context pass ran (missing managed context receipt, elapsed=1ms) #663
Description
Summary
The OMP dispatch fence in registerPiGuardedContext aborts a turn when before_provider_request fires without a preceding context pass for that operation. The user-visible result is a refused turn:
Magic Context could not safely prepare this turn; send your message again.
stage=provider dispatch elapsed=1ms recovery=no managed result; refused (missing managed context receipt)
The turn is then killed via ctx.abort(). This happens intermittently in long-running sessions and also stops delegated subagents mid-run.
Environment
@cortexkit/pi-magic-context0.47.0 (latest as of this report)- Harness: OMP (
oh-my-pi), soresolvePiHarnessKind() === "omp"and the fence is enabled - Reported against real sessions; the diagnostic signature below is quoted verbatim from
ctx.ui.notify(...)
Root cause
In src/pi-context-refusal.ts, the before_provider_request handler does:
const active = operation;
const isCurrent = () => operations.get(key) === active;
const budget = active.pass ?? new PiContextBudget; // <-- fresh budget when no pass ran
try {
budget.assertOwner();
if (!budget.completed)
budget.assertOutcome();
if (!active.ready || active.refused || active.ended || budget.abandoned)
throw new Error("missing managed context receipt");
} catch (error) {
active.refused = true;
budget.stage = "provider dispatch";
refuse(ctx, budget, error, isCurrent);
}active.pass is assigned in exactly one place — inside the context handler:
const active = operation;
active.pass = budget;
active.ready = false;So when before_provider_request runs for an operation that has never had a context pass:
active.passisundefinednew PiContextBudgetis constructed withstartedAt = performance.now(), soelapsed()is ~0active.readyisfalse(nothing ever set it)throw new Error("missing managed context receipt")refuse(...)→ctx.abort()(since!budget.sideTurn)
The observed elapsed=1ms is the fingerprint of step 2: a budget minted at dispatch time rather than carried over from a pass. A budget that actually came from a context pass would report elapsed in the hundreds/thousands of ms, because the pass itself does real work first.
pi.on("agent_start", ...) makes this easier to hit, since it replaces the operation with a fresh one every time:
pi.on("agent_start", (_event, ctx) => {
operations.set(operationKey(ctx), newOperation()); // { ready: false, refused: false, ended: false }
});Any before_provider_request that arrives for that key without a matching context pass therefore refuses.
Why this is the wrong behaviour
If no context pass has run, Magic Context has not produced any managed result for this turn — so there is nothing to lose by letting the provider request through. The fence is supposed to protect against a stale or superseded pass leaking into the provider payload; it cannot protect against a pass that never existed. Aborting the user's turn (and any running subagent) is a strictly worse outcome than admitting an unguarded request.
Local workaround applied
Patching the vendored dist/index.js to bail out when no pass exists:
const active = operation;
const isCurrent = () => operations.get(key) === active;
// `active.pass` is assigned ONLY inside the `context` handler. When it is
// undefined no context pass has ever run for this operation, so there is
// no managed result to admit or reject — refusing here aborts a turn Magic
// Context never touched.
if (!active.pass) return;
const budget = active.pass;This is a node_modules edit and will be lost on reinstall, hence filing upstream.
Suggested fix
Guard the fence on the pass actually existing, and only refuse when a pass ran but produced no receipt:
if (!active.pass) return; // no pass ran → nothing to admit/reject
const budget = active.pass;If refusing an unguarded dispatch is genuinely required on the OMP harness, consider distinguishing the two cases in the diagnostic (no context pass ran vs pass ran but produced no receipt) so the failure is actionable rather than indistinguishable from a superseded pass.
Related
The same file also emits Pi context pass superseded before writer admission from two sites (budget.assertOwner overrides). Those fire on a genuinely superseded pass and appear to be a different, intentional condition — not covered by this report.
Activity
magic-alfonso commentedon Oct 11, 2026
Thanks for the clear trace. The
elapsed=1msreading is right: that budget was created at dispatch time, so nocontextpass ran for that operation before the provider request.We're reproducing it on a real Oh My Pi host to find which request paths reach
before_provider_requestwithout acontextpass for the same operation. That could be a host path that skips thecontextevent, or our own operation tracking (for example anagent_startreplacing the operation between the pass and the dispatch).One caution about the local patch: the fence exists so that a long session's full, unmanaged history is never sent when Magic Context's pass didn't run or didn't finish. With the patch, those requests go out unmanaged. On a large session that can exceed the model's window or rewrite the whole prompt cache. It's fine as a stopgap, but watch for that. We'll post the findings and the fix here.
Qiiks commentedon Oct 11, 2026
Second failure mode: superseded pass also aborts the turn
Same fence, different reason string, and a very different diagnostic fingerprint:
Contrast with the first report
Pi context pass superseded before writer admissionmissing managed context receiptelapsedThe multi-second
elapsedis the tell:active.passexists and is stale, sobudget.assertOwner()is the thrower, not the receipt check.Why it still kills the turn
refuse()ends with:That runs for every reason, including
PiContextSupersededError. But supersession means a newer context pass already took over for the same session (contextPassGenerationwas bumped, oroperations.get(key)now resolves to a later operation). Magic Context is demonstrably alive and a fresher result exists — yet the turn is aborted to defend against a staleness the newer pass has already resolved.Note the stage label is misleading here:
before_provider_requestoverwritesbudget.stage = "provider dispatch"in its catch, so a throw that originated from a context-passassertOwner()is always reported as a dispatch-stage failure.Local workaround applied
Early-return in
refuse()so supersession is logged rather than fatal:Again a
node_modulesedit, lost on reinstall.Suggested fix
Supersession is a liveness signal, not a corruption signal. It should not share a fatal path with "a pass ran and produced nothing". At minimum the two should diverge in
refuse(); better, distinguish them in the diagnostic sostage=provider dispatch elapsed=64543msstops being reported as the same class of failure aselapsed=1ms.magic-alfonso commentedon Oct 11, 2026
Thanks, this is a separate path, and the elapsed time is a useful clue: a pass did run, so
assertOwner()threw at dispatch rather than the receipt check.A note on the first report, since it may also apply to your setup: we reproduced the 1 ms refusal on Oh My Pi 18.8.6. One cause was another extension delaying
agent_startuntil after Magic Context had prepared the turn, so the handler discarded that preparation. A second cause was Oh My Pi's advisor requests being checked as if they were the conversation. Both are fixed on master and ship in the next release.For this supersession case, we're now reproducing which sequence leaves a 60–115 s old pass's request reaching dispatch. The fix depends on whether the request being sent still matches what that pass prepared: then it can go through. If a newer pass's result is the one that should be sent, the stale request has to be refused or replaced rather than sent unmanaged. Your workaround sends it either way, so on a large session it can send history Magic Context hasn't reduced. We'll also fix the stage label you pointed out.
magic-alfonso commentedon Oct 12, 2026
Fixed in 0.48.0. We reproduced both refusals on a real Oh My Pi host. The 1 ms 'missing managed context receipt' came from another extension delaying
agent_startuntil after Magic Context had prepared the turn, so the handler discarded that preparation; the first late start now keeps it. Oh My Pi's advisor requests, which carry their own formatted history, are now recognized and no longer use up or require the turn's receipt. Your second report (the 64–115 s 'superseded' refusal) was the same advisor path checked against an already-finished turn, so 0.48.0 covers it too. You can drop the local patch after updating.