Skip to content

pi-magic-context 0.48.0: dispatch fence escalates a superseded context pass into a turn refusal ("Pi context pass superseded before writer admission") #667

Closed

Description

@Qiiks

Magic Context turns fail with "Pi context pass superseded before writer admission"

Plugin: @cortexkit/pi-magic-context v0.48.0
Repo: https://github.com/cortexkit/magic-context (packages/pi-plugin)
Observed on: omp 18.7.0, CachyOS Linux, plugin log /tmp/omp/magic-context/magic-context.log

Symptom

A user turn aborts with:

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

Resending the message clears it.

Verified code path

All line numbers are dist/index.js.

The string Pi context pass superseded before writer admission exists once, in
PiContextBudget.assertOwner (31959-31960):

const isCurrent = () => isOwned() && (budget.sideTurn || !active.ended);
budget.assertOwner = () => {
  if (!isCurrent())
    throw new PiContextSupersededError("Pi context pass superseded before writer admission");
};

The user-visible message is assembled by refuse() (31849-31871), which assigns
both strings appearing in the report:

  • budget.recovery = "no managed result; refused" (31858)
  • budget.stage = "provider dispatch" is set by the caller at 31926
  • message = ${RETRY_MESSAGE} ${budget.diagnostic()} (${reason.message})`` (31861)

Critically, refuse() only produces user-visible output when isCurrent() is
true (31850-31853):

const refuse = (ctx, budget, reason, isCurrent) => {
  if (!isCurrent()) {
    log2(`[magic-context][pi] DISCARDED CONTEXT RESULT ${budget.diagnostic()}`, reason);
    return;                      // no notify, no abort - silently inert
  }
  ...
  ctx.ui?.notify(message, "error");
  pi.appendEntry(ENTRY_TYPE, { message });
  ctx.abort();
};

So the error reaching the user proves isCurrent() was true when it was
raised — the pass was not superseded from the perspective of the assertion
that produced it.

The actual throwing site

Log events matching the reported error (3 in the current log) carry this stack:

Error: Pi context pass superseded before writer admission
    at <anonymous> (index.js:31279:19)
    at <anonymous> (index.js:35932:7)
    at assertOutcome (index.js:31038:10)
    at assert (index.js:31043:10)
    at <anonymous> (index.js:31068:7)
    at async <anonymous> (index.js:35949:31)

Frame 31068 is inside injectM0M1Pi:

function injectM0M1Pi(state, db, piMessages, entryIds, recomputeM1ThisPass = false, passSnapshot) {
  if (state.preparedPrefix) { ... }
  const snapshot = passSnapshot ?? createPiM0M1PassSnapshot({   // <- 31067-31068
    db,
    sessionId: state.sessionId,
    ...
  });

and assert/assertOutcome (31038-31043) are the budget.assertOutcome
alias bound at 37784 (let assertCurrentPass = budget.assert). The pass guard
therefore throws inside the M0/M1 transform pipeline, i.e. during transform
work — not at the before_provider_request dispatch fence, and not in
flushPiServedArrayLedger.

This matters for the fix: the assertion is evaluating a newer pass while the
older pass is still doing transform work, because the transform runs long enough
(a transform completing in 630 ms over 1028 messages is typical here) for the
next turn's pass to start and replace operations.get(key).

Two distinct log classes — do not conflate them

Class Log string Site User-visible?
A. benign supersession DISCARDED CONTEXT RESULT: pending Pi transform decision (76×) dynamic budget.diagnostic() at 31971 no
B. the reported bug DISCARDED CONTEXT RESULT stage=provider dispatch elapsed=… recovery=no managed result; refused Pi context pass superseded… (3×) dynamic string at 31851, thrown from the transform pipeline yes
C. genuine refusal turn refused stage=provider dispatch elapsed=79ms recovery=no managed result; refused (1×) 31976, error not a PiContextSupersededError yes
D. ledger flush DISCARDED CONTEXT RESULT: queued served capture (5×) static string 33348, inside flushPiServedArrayLedger no

Class D's static string looks like class B in a log grep, but it is a different
call site with a different cause and never reaches the user. Class B is the one
that produced the reported error.

Why it is intermittent

Reproduction conditions observed (not proven general):

  • Sessions with a long context. The failing session was at
    usage=46.5% (435645 tokens) with context_passes=270, side_contexts=1,
    dispatch_fence=on.
  • The transform pipeline reaches a pass guard that asserts (passGuard.assert()
    appears at 25138, 25157, 25185, 25191, 25217, 25230, 25245, 25249, 25278,
    25284, 25298, 25303, 31660, 31671, 38364) after a newer pass has already
    replaced the operation. Whether that happens depends on whether a new pass
    starts during the transform — hence the rarity (3 events against 76 benign
    discards).
  • elapsed=54630ms is transform time. No provider request is made on this path,
    so no provider, WARP-proxy, or network behaviour is involved.

Suggested fix

The failure is that a pass doing long transform work can be superseded by a
newer pass and, when the older pass's next guard assertion fires, the error is
routed into refuse() and surfaced as a turn failure instead of being treated
as an abandoned pass.

The cleanest fix is in the transform pipeline's guard handling: once a pass is no
longer isCurrent(), its remaining work should unwind quietly (the newer pass
owns the session) rather than escalating into refuse(). Concretely, wrap the
transform pipeline so that a PiContextSupersededError raised after the pass has
been superseded is caught and discarded:

try {
  return injectM0M1Pi(state, db, piMessages, entryIds, recomputeM1ThisPass, passSnapshot);
} catch (error) {
  if (error instanceof PiContextSupersededError && !budget.isCurrent()) {
    log2(`[magic-context][pi] abandoned superseded pass: ${state.sessionId}`);
    return undefined;               // let the newer pass win
  }
  throw error;
}

An alternative (cheaper, less precise) is to give the older pass an abort signal
at supersession time so the guard raises a cancellation instead of a
supersession error — but that still needs the caller to distinguish
"cancelled because a newer pass won" from a real failure.

Notes for the reporter

  • stage=provider dispatch in the message is a label assigned at 31926, not
    proof a provider call happened.
  • Class D (flushPiServedArrayLedger, 33348) discards served-ledger lines and is
    a separate, benign mechanism; fixing it is not required for this bug.
  • Log: /tmp/omp/magic-context/magic-context.log (14 MB). Store:
    ~/.local/share/cortexkit/magic-context/context.db.

Activity

  1. Qiiks commented on Oct 12, 2026

    @Qiiks
    ContributorAuthor

    Correction: the fix belongs in the dispatch fence, not the transform

    Two reviewers pushed back on my original fix and they are right. Correcting the
    analysis here — the original report below is updated inline where needed.

    The decisive fact

    stage = "provider dispatch" is assigned in exactly one place in
    dist/index.js, at line 31926:

    $ grep -c 'stage = "provider dispatch"' index.js
    1
    

    diagnostic() only interpolates this.stage, so every log line or user message
    carrying stage=provider dispatch was assembled from a budget whose .stage
    was set at 31926 — inside the before_provider_request fence catch
    (31915-31927). That catch is unconditional:

    } catch (error) {
      active.refused = true;
      budget.stage = "provider dispatch";   // <-- the only assignment
      refuse(ctx, budget, error, isCurrent);
    }

    So the transform-pipeline frames in the stack trace show where the error was
    thrown
    , not where the user-visible refusal was assembled. My original report
    conflated the two, and my original fix is therefore ineffective.

    Second error: refuse() reached the notify path, so isCurrent() was TRUE

    refuse() (31849-31863):

    const refuse = (ctx, budget, reason, isCurrent) => {
      if (!isCurrent()) {
        log2(`[magic-context][pi] DISCARDED CONTEXT RESULT ${budget.diagnostic()}`, reason);
        return;                                     // inert: no recovery set, no notify
      }
      budget.abandoned = true;
      budget.recovery = budget.recovery === "not attempted"
        ? "no managed result; refused" : budget.recovery;   // <-- 31858
      ...
      ctx.ui?.notify(message, "error");              // <-- 31861
      ...
      ctx.abort();
    };

    The logged class-B lines contain recovery=no managed result; refused, and that
    string is only assigned at 31858 — i.e. inside the branch that continues to
    ctx.ui.notify() at 31861 and ctx.abort(). The !isCurrent() branch at
    31850-31853 returns before assigning recovery. So the failing events took the
    notify path, meaning isCurrent() returned true at the fence.

    That also corrects the table in the original report: the log line at 31851 is the
    inert branch (31851 logs the dynamic ${budget.diagnostic()} and returns), and
    class B's user-visible behaviour belongs to 31861, not 31851.

    Why my transform-pipeline fix cannot work

    The fence does not re-read the transform's outcome. Its first assertion is
    budget.assertOwner() (31913), which throws the same
    PiContextSupersededError on the same !active.ended condition. Wrapping
    injectM0M1Pi to swallow that error changes nothing about what the fence sees —
    the same budget object still fails assertOwner() at 31913 and still takes the
    unconditional catch. Swallowing inside the transform leaves the fence fully
    intact, so the user is still notified and aborted.

    The fix

    Break the escalation at the fence. A superseded pass is an abandoned pass, never
    a turn refusal:

    } catch (error) {
      // A superseded pass is inert: a newer pass owns the operation now, so this
      // must not escalate into a user turn refusal.
      if (error instanceof PiContextSupersededError || !isCurrent()) {
        log2(`[magic-context][pi] abandoned superseded pass before dispatch: ${operationKey(ctx)}`);
        return;
      }
      active.refused = true;
      budget.stage = "provider dispatch";
      refuse(ctx, budget, error, isCurrent);
    }

    Keeping active.refused = true inside the early-return branch would also be
    defensible, but the essential change is that refuse() — and therefore
    ctx.ui.notify() and ctx.abort() — is not reached for a supersession.

    Evidence for the reproduction conditions

    All three logged instances have the same shape: the line
    OMP context counts session=… context_passes=N dispatch_fence=on immediately
    precedes the failure, and in two of three a
    checkout claim: WARNING check incomplete, admitting reason=… line precedes
    that, with reason=timeout or reason=error stage=connect detail="Error: server pro….
    718 such checkout-stall warnings exist in the same log, so a stall alone is not
    sufficient — the pass must then find itself non-current at the fence.

    Original report (retained below, with the noted corrections applied)

  2. changed the title [-]pi-magic-context 0.48.0: turn refused with "Pi context pass superseded before writer admission" raised from the M0/M1 transform pipeline[/-] [+]pi-magic-context 0.48.0: dispatch fence escalates a superseded context pass into a turn refusal ("Pi context pass superseded before writer admission")[/+] on Oct 12, 2026
  3. magic-alfonso commented on Oct 12, 2026

    @magic-alfonso

    Thanks for the detailed trace and for correcting it yourself. We're reproducing this on a real Oh My Pi host now. One constraint shapes the fix: the dispatch fence exists so that OMP never sends a request Magic Context didn't prepare. So the fence can't simply return when a pass turns out to be superseded. It has to confirm that the request about to go out is the newer pass's managed result, and only then let it through. We'll check where the superseded error is raised, why the fence still saw the pass as current, and why that pass ran for 54 s. We'll post what we find here.

  4. magic-alfonso commented on Oct 12, 2026

    @magic-alfonso

    Fixed on master; it ships in the next release.

    Nothing was actually superseded. Oh My Pi delivers the previous run's agent_end to extensions late, after the next run's context pass has already returned its managed result. The dispatch fence treated that late end as ending the current run, so the next request was refused with the superseded message. We reproduced it on real Oh My Pi 18.7.0 and 18.8.6 hosts with the released 0.48.0 build. A second, rarer path produced the same text: a pass that waited for the database past its deadline could start late and claim the session, making the newer pass look superseded.

    What changed:

    • A late agent_end from an earlier run no longer ends the current run's pass. A run's own end still invalidates its result, including when the run is aborted before its request goes out.
    • The fence decides from the latest pass's state. The request it lets through is always that pass's managed result; otherwise it still refuses visibly, as before.
    • The 25 s pass budget now covers the wait for the database, so a pass is refused before Oh My Pi's 30 s deadline rather than running past it, and a late-starting pass stops before it writes anything.

    Your stack trace matches 0.47.0 line numbers rather than 0.48.0, so your log mixes events from both versions. The 0.47.0 checkout-claim check that preceded two of your three events is already gone in 0.48.0, but the bug reproduces without it.

  5. magic-alfonso commented on Oct 13, 2026

    @magic-alfonso

    Fixed in 0.49.0. A context pass that is replaced by a newer one no longer causes the newer, prepared turn to be refused, including when the previous turn ends late. An unreadable message list now produces a visible refusal instead of an error escaping the context hook. Update @cortexkit/pi-magic-context and restart Oh My Pi.

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