Skip to content

[pi-plugin] Dispatch fence aborts turns when no context pass ran (missing managed context receipt, elapsed=1ms) #663

Closed

Description

@Qiiks

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-context 0.47.0 (latest as of this report)
  • Harness: OMP (oh-my-pi), so resolvePiHarnessKind() === "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:

  1. active.pass is undefined
  2. new PiContextBudget is constructed with startedAt = performance.now(), so elapsed() is ~0
  3. active.ready is false (nothing ever set it)
  4. throw new Error("missing managed context receipt")
  5. 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

  1. magic-alfonso commented on Oct 11, 2026

    @magic-alfonso

    Thanks for the clear trace. The elapsed=1ms reading is right: that budget was created at dispatch time, so no context pass 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_request without a context pass for the same operation. That could be a host path that skips the context event, or our own operation tracking (for example an agent_start replacing 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.

  2. Qiiks commented on Oct 11, 2026

    @Qiiks
    ContributorAuthor

    Second failure mode: superseded pass also aborts the turn

    Same fence, different reason string, and a very different diagnostic fingerprint:

    Error: Magic Context could not safely prepare this turn; send your message again.
    stage=provider dispatch elapsed=64543ms recovery=no managed result; refused (Pi context pass superseded before writer admission)
    
    Error: Magic Context could not safely prepare this turn; send your message again.
    stage=provider dispatch elapsed=115468ms recovery=no managed result; refused (Pi context pass superseded before writer admission)
    

    Contrast with the first report

    this report #663 (first)
    reason Pi context pass superseded before writer admission missing managed context receipt
    elapsed 64543ms / 115468ms 1ms
    meaning a pass did run, long ago no pass ever ran

    The multi-second elapsed is the tell: active.pass exists and is stale, so budget.assertOwner() is the thrower, not the receipt check.

    Why it still kills the turn

    refuse() ends with:

    } finally {
        if (!budget.sideTurn)
            ctx.abort();
    }

    That runs for every reason, including PiContextSupersededError. But supersession means a newer context pass already took over for the same session (contextPassGeneration was bumped, or operations.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_request overwrites budget.stage = "provider dispatch" in its catch, so a throw that originated from a context-pass assertOwner() is always reported as a dispatch-stage failure.

    Local workaround applied

    Early-return in refuse() so supersession is logged rather than fatal:

    if (reason instanceof PiContextSupersededError) {
        log(`[magic-context][pi] superseded pass admitted at dispatch ${budget.diagnostic()}`, reason);
        return;
    }
    budget.abandoned = true;
    // ...

    Again a node_modules edit, 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 so stage=provider dispatch elapsed=64543ms stops being reported as the same class of failure as elapsed=1ms.

  3. magic-alfonso commented on Oct 11, 2026

    @magic-alfonso

    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_start until 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.

  4. magic-alfonso commented on Oct 12, 2026

    @magic-alfonso

    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_start until 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions