You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
pi dispatch fence: piPreparedStructure rejects MC's own leading system injection (m0/m1), refusing every injected turn #686
On OMP, every turn of a session that receives MC memory injection (m0/m1) is refused at provider dispatch:
refused (provider request does not match this pass's managed dispatch (prepared turn structure))
Fresh sessions (no injection) work fine. The cause is an asymmetry between MC's own snapshot and verify functions: the snapshot rejects the very system-role messages MC itself injects.
Environment
@cortexkit/pi-magic-context0.49.1 (installed dist/index.js sha256 fc3d7cc3…f76e — byte-identical to the published tarball, so this is the current release, not a stale build)
DB healthy (integrity_check: ok, rebuilt from salvage)
Mechanism (traced in dist/index.js)
injectM0M1Pi prepends the memory blocks as leading role: "system" messages (isPiSystemEntry === role === "system", counted via syntheticLeadingCount). Log from a failing turn: injected m[0]/m[1] into Pi messages (221199 + 90 bytes, materialized=false)
The context handler then snapshots: preparedStructure = piPreparedStructure(result.messages).
messageStructure maps custom|hookMessage|developer|… → "user", but anything else must be exactly user|assistant|toolResult — "system" hits return → undefined, so piPreparedStructure bails and prepared is undefined.
Meanwhile payloadStructure explicitly skips leading system/developer rows, so the payload side is well-defined.
The dispatch check treats !prepared as mismatch → refusal. Signature check passes (managed content is present); only the structure snapshot is broken.
Mirror the payload reader's skip rule in piPreparedStructure (verified locally with before/after unit test — snapshot and payload rows then agree, normal turns and exotic-role rejection unchanged):
Thank you. Your diagnosis is right, and it's the root cause behind 683 and 684. Pi's history injection prepends role: "system" messages; piPreparedStructure rejects that role, while the payload reader skips leading system rows. So every session that has injected history gets refused on Oh My Pi, while fresh sessions pass. That's also why our provider tests, which used fresh sessions, missed it.
The fix applies the payload side's leading-system skip on the prepared side too, and makes developer agree on both sides. It adds a regression test for exactly this case, and we're rerunning every provider API on a real Oh My Pi 18.10.4 host with history injected. It's going into 0.49.2 now, together with five other provider-shape fixes, so there's no need for a PR, though thanks for offering. We'll credit you in the release notes. Your local two-line patch is a safe workaround until then.
A correction and a question. We checked the published 0.49.1 package against your build: your dist digest matches it exactly. In that build, Magic Context's own history head (m0/m1) is injected as user-role messages, not system. It inserts them after any role: "system" entry that's already at the head of the session's messages, keeping that entry in place. So the leading system row that trips the check is already in your session's input, and our prepared-side reader rejects it while the request side skips it. Your fix (skip leading system rows on both sides) is still the right one, and it's what 0.49.2 does.
To make sure we cover the source of that row, do you know what adds a system-role entry to your Oh My Pi sessions? For example: another extension, a custom system-prompt or rules setting, or a compaction or summary feature. If you're not sure, the name of any other Oh My Pi extensions you have enabled would help.
Follow-up from live debugging: the leading-system skip (your 0.49.2 fix, which I verified locally) resolves the !prepared refusal, but on this host a second divergence remains and still refuses every turn on sessions with injected history.
Instrumented piMatchesPreparedStructure on 0.49.1 + local system-skip, one real OMP 18.10.4 turn, zen/chat-completions lane, ~867-message window:
payload rows: 797, prepared rows: 867 (stable gap of 70 across turns)
role histograms identical except assistant[c0r0]: 58 in payload vs 128 prepared
all 128 prepared-side no-call assistant texts carry textSignature (key shape text+textSignature+type, no other markers); storage shows e.g. textSignature: {v:1, id:"rs_…", phase:"commentary"} on these blocks
stored history mixes api: "openai-responses" and "openai-completions" entries (session moved lanes over its life); some carry cached providerPayload: {type: "openaiResponsesHistory", …}
So 70 text-bearing, signed assistant messages present in result.messages never reach the chat-completions wire body. Smells like the responses→completions history conversion dropping signed/commentary texts (possibly one of the five provider-shape fixes you mentioned, but flagging in case it isn't covered). Happy to capture any further diagnostics you need — the instrumented build logs STRUCTDBG count/hist rows to the magic-context log.
Diagnostic evidence from an isolated fork (session copied to a new ID, tested via omp -p --resume, live session untouched), with an instrumented piMatchesPreparedStructure logging counts, role histograms, and multiset text diffs (STRUCTDBG lines in the magic-context log):
Stable net gaps, same shape. Fork window: payload rows 1089 vs prepared 1095 (gap 6). Live session window: 797 vs 867 (gap 70). Role histograms identical on both sides except assistant[c0r0] (fork: 40 vs 46; live: 58 vs 128). Everything else matches exactly, so this is content-rule loss of one message class, not window drift.
The missing messages are exactly the §N§-tagged assistant texts. Multiset diff of prepared fingerprints against payload fingerprints: every prepared-side fingerprint absent from the payload starts with §<digits>§ (e.g. §12§ JWT is live (200)…, §36§ Confirmed against kernel source…, ranges §12§–§647§ in the sampled windows). Payload-side assistant items with tags (§10104§… and up) ARE present — so there are two tag series, and only the small-number series vanishes.
The small-tag series is runtime-generated, not stored. Grep over the full session JSONL (28k lines, 67MB) finds zero occurrences of the small-tagged texts (e.g. §12§ JWT is live); they exist only in result.messages. MC dist contains no code that prepends §N§ summaries (only the served-cache validator that recognizes[dropped §N§] / §N§ prefixes), so these look like replayed/restored rows (dropped-placeholder restore? fold restore?) injected by the transform pipeline. The large-tag series IS in storage (233 assistant messages starting with §), i.e. tags persisted from prior served turns.
Wire side. The zen lane payload is responses-shaped (body.input with function_call / function_call_output items, 1033 items). The chat converter rule I extracted from the host (text non-blank → keep; drop iff empty && no tool_calls && no reasoning) would keep all 46 texts, so they never reach conversion input — consistent with OMP building the wire list from pre-handler messages while the 46 are handler-added. Untagged-twin check: stripping the §N§ prefix from the 46 and searching payload texts finds only 1/40, so they are not present untagged either.
Questions for your side: (1) which pipeline stage emits the small-tag §N§ assistant summaries into result.messages, and (2) is OMP supposed to forward handler-added messages to the wire, or should the snapshot exclude them? Instrumented build available if you want specific additional fields captured — just say what to log.
Root cause identified — with proof.result.messages handed to the snapshot IS MC's own stored last-served array. I read it out of the shared store and compared it against the instrumented snapshot:
So the prepared snapshot is taken over MC's LKG-resident served array, while the payload is built by OMP fresh from session entries. OMP's conversion loses 70 of those assistant texts (payload assistant[c0r0]=58), which is the entire gap. Small-§N§ summaries are NOT involved on this session's prepared side (0 in stored array) — earlier log samples showing them came from a different window state.
Since custom and developer roles map to user in messageStructure, the 125 injected guidance blocks survive conversion, but the 128 assistant texts do not survive it — they are present in MC's array and absent from the wire body. That is the mismatch.
Why your provider tests miss it: a fresh session has an empty last-served array (no LKG, no custom/developer injections, 128 vs 0 assistant-text rows), so prepared == payload and everything passes. Only sessions with MC-managed history hit this.
multiset diff: 80 prepared-side assistant texts absent from payload, all tagged §N§
We can choose between two fixes, and we want your direction:
Vendor patch (preferred): make piPreparedStructure snapshot the payload side's post-conversion view, or have OMP feed the transform result into the wire body so result.messages IS the payload. Either closes the gap for all hosts.
Local workaround (for this session now): patch the local piPreparedStructure to tolerate the class of row loss the converter causes (e.g. ignore assistant text-only rows when the payload is responses-shaped). We'd want to know if you see any security/correctness risk in treating that class as "expendable for identity checks", because they're not tool-call/tool-result rows.
Please advise which you want us to run. If it is #1, do you want any further captures from the instrumented build — we can log the payload-side pre-conversion message array too, which would nail the exact converter rule dropping the 70.
Thank you, this is exactly the evidence we needed. To your two questions:
The §N§ prefixes are Magic Context's own tags. Every message part gets one at runtime when the context is prepared, and they're stored only in Magic Context's database, not in the Pi session file. Small numbers are simply older messages. So the tags are expected and not the cause. The higher-numbered tags you found stored in the session file are ones the model copied into its own replies.
Oh My Pi sends the messages our handler returns, otherwise none of our trimming would reach the wire and the payload would be larger, not smaller. So the 46 and 70 missing rows are being left out by Oh My Pi's openai-responses request builder. Your evidence points at the signed phase: "commentary" assistant texts and the cached openaiResponsesHistory from the lane switch. We're tracing exactly which rule in Oh My Pi 18.10.4 does it, reproducing it on a real host, and matching that rule in the check, without loosening it for anything else.
0.49.2 is going out first with the leading-system fix and the other provider-shape fixes, because they're already verified and help other setups. Your session needs this second fix as well, so this issue stays open, and the follow-up comes as 0.49.3. If we need fields from your instrumented build to confirm the rule, we'll ask here.
Understood and thanks for the precise correction — that resolves the two things we had wrong:
Confirmed: §N§ is MC-runtime tagging, small = older, stored in the MC DB, not the session file. Not the cause. (Our session-file grep for small-tag texts finding nothing is consistent with this: the tags were never in the file at all, and the higher-numbered ones in the file are the model copying them into replies — that also explains why our "untagged twin" multiset check failed.)
More importantly: OMP does forward the handler's returned messages, so the lost rows are being dropped by OMP's openai-responses request builder, not by MC. We'll stop treating it as an MC-side snapshot bug and will mirror your 18.10.4 trace.
Two things we can offer to help that trace, since we have both an instrumented build and a reproducer on real hardware:
a) Payload-side pre-conversion capture. We can extend the STRUCTDBG instrumentation to log the array as it reaches the responses builder (before item conversion), plus the exact final payload.input items corresponding to the lost rows. If that would pin the rule faster than your repro, say the word and we'll capture and post it verbatim (redacted if needed — currently logging text prefixes only, ~60 chars, which we can also raise or lower).
b) Version pinning. We're on OMP 18.10.4 exactly, single instance, free-tier zen lane, muse-spark-1.3-contributor-free. If you bisect 18.10.x, we can run the same fork turn against any specific build you name within minutes, since the fork + -p harness is already set up.
Regarding the local patch we were about to apply: we're shelving it for now, since your direction says the correct fix is matching OMP's exact drop rule rather than tolerating a class of loss. That's the safer call and we'd rather wait for your 0.49.3 rule match than ship a locally-loosened guard. The instrumented build stays live in the meantime.
We're on the clock with a real project deadline on the affected session, so we'd like to converge on a patch this hour if possible. Three asks, most specific first:
1. The exact drop rule. If you've traced which rule in OMP 18.10.4's openai-responses builder drops these rows, sharing it lets us mirror it exactly rather than guess. Our best current suspects from reading the host dist, in order:
The cached-history replay path (openaiResponsesHistory → Zhe()/xee() items) rebuilding items only for a subset — e.g. replaying function_call/function_call_output pairs and reconstructing assistant text items only when a round-trip verifies.
Signed phase: "commentary" texts being dropped when a model/api pair doesn't accept thoughtSignature-style metadata on replay (we see textSignature: {v:1, id:"rs_...", phase:"commentary"} on most of the lost texts, and a few of the survivors have no signature at all).
A rule keyed on requiresReasoningContentForToolCalls or allowsSyntheticReasoningContentForToolCalls interacting with reused provider history.
2. A blessed local patch shape. Given the deadline, we'd like to apply your approved local patch rather than invent one — our earlier attempt to tolerate "assistant text-only row loss" is exactly the kind of loosening you said you didn't want, and we shelved it. What we'd like to run instead is the mirror-the-rule version: in piPreparedStructure, apply the same transform you're matching in 0.49.3 to the prepared rows, so the snapshot equals the post-conversion payload. If you can tell us that transform (even pseudocode of the drop condition), we'll implement it against the real code and send you the diff before enabling it, so you can veto.
3. Fallback if the rule isn't pinned yet: is there a minimal, vendor-blessed way to make the affected session usable today without the guard change — e.g. a dispatch_fence / managed_dispatch config toggle that defers gracefully (log + send) instead of refusing, so the turn proceeds while we lose the MC guard on that one session only? If such a switch exists in 0.49.1 (even an env var), tell us the name and we'll use it for the deadline, then revert.
We can capture and post anything you need from the real host within minutes — including logging the pre-conversion message array that reaches the responses builder, which we haven't instrumented yet but can add in one build.
Update from the source trace, since you're matching the exact rule. We dug into OMP 18.10.4's dist and found the actual message-dropping rules in the responses request builder. Posting them verbatim so you can confirm/correct rather than re-derive.
Where the drop happens. The responses input is built by Qc(e.context.messages, e.model, kJn) first (normalization + tool-pairing), then the per-message loop emits items. Three rules can delete whole messages:
3. toolResult orphan/stale/duplicate drops (relevant to tool-identity but not to assistant text):
if(x.has(_))continue;// already consumedif(!h.has(_)){if(T.some(...)||b.size>0)continue;/* stale-tool-result -> becomes a user text row */}
4. Cached-history replay path (when providerPayload.type==="openaiResponsesHistory" and api+model match the current model): if nativeHistory.replay===true or RQi(items) holds, output is built from the cached itemsonly — See() scrubs assistant messages whose text is whitespace-only (x3n) and g$i() removes those plus adjacent reasoning; then jCt is skipped entirely. Non-matching api/model (or no cached payload) falls through to jCt, which does keep text blocks (it re-derives msg_* ids and drops phase/textSignature metadata).
Our empirical check against your hypothesis: on our fork, the prepared side's 46 text-only assistant messages are all stopReason:"stop", model:"muse-spark-1.3-contributor-free", and they all do have a cached message item containing the text. So rule 4 (with its x3n/g$i scrub) does not account for them, and m3n can't fire on them either because they carry text. Which is why we're asking rather than guessing.
Two questions, and then we'll stop poking:
a) Is the 70-row loss entirely explained by m3n + n$i acting on the session-file entries OMP sends us (our count of m3n candidates in the live session's tail window is 21-23 depending on window size, so no, not entirely), or is there a rule we haven't listed?
b) If you give us the rule (or a pseudocode predicate over the internal message shape), we'll implement the mirror in piPreparedStructure, post the diff here for your veto, and only enable it after you sign off. If you'd rather we not touch the guard at all, we'll wait for 0.49.3 — just confirm which, because we have a same-day deadline on this session and need to commit to one path.
Thank you for the source trace; it saves us a lot of time, and we've passed it to the engineer reproducing this on a real Oh My Pi 18.10.4 host.
(a) We don't have the full rule yet. Your counts already show that m3n and n$i don't explain all 70 rows. One thing we're checking first: whether the cached-history replay emits one response's cached items only once when several consecutive assistant messages came from that same response (for example a commentary text followed by the final answer). That would remove rows while keeping the text, which fits what you saw. Treat that as a hypothesis until we've confirmed it on the host.
(b) Please don't patch the check locally. It's what stops Oh My Pi from sending a request Magic Context didn't prepare, and a mirror rule that's slightly too broad would let that through without any sign. For a same-day deadline, the safe option is to pin @cortexkit/pi-magic-context to 0.49.0, which doesn't have this check. Its remaining failure mode is the rarer 673/674 refusal, which a resend clears. We'll post the exact rule here as soon as it's confirmed, and the fix ships as 0.49.3.
We've confirmed the cause on a real Oh My Pi 18.10.4 host, and it's in Magic Context, not Oh My Pi. Oh My Pi does send all of those assistant texts: the cached-history replay pushes every matching assistant's items (openai-shared.tspushNativeReplayItems) and doesn't deduplicate them by response. Our check is what lost them. On the responses lane, the request reader merged consecutive assistant items into one row, but the prepared side kept every assistant message. A run of assistant messages, like your commentary texts followed by the answer, therefore looked like missing rows. Your STRUCTDBG counts were measuring those merged rows.
The fix groups adjacent assistant rows the same way on both sides and keeps every text item. A request that really drops one of those texts, or wasn't prepared by Magic Context, is still refused. Your exact shape (signed commentary, cached responses history, a lane switch) reproduces red on the real host and passes with the fix.
It ships in 0.49.2, together with the leading-system fix; we're holding that release for it. Until then, pinning 0.49.0 remains the safe option. Thanks again: your counts and trace led straight to it.
We found the root cause, verified it on real data, and have a patch ready. It is not an OMP rule at all — it is MC's own payloadStructure merging consecutive assistant rows. Full disclosure of how we got there, because it corrects the direction we pointed you in earlier.
For a Responses payload, this folds every assistant row into the previous assistant row when the previous row is also an assistant. Per text item that is fine, but a history containing consecutive text-only assistant turns loses one row per run: two adjacent text-only turns collapse into one row. The prepared snapshot keeps both turns, the payload keeps one, and the fence refuses with prepared turn structure.
The function_call branch above it already attaches calls to the preceding assistant row, so this merge only ever fired for text-item-following-text-item — exactly the case where it is wrong.
Proof on real data
We instrumented nothing in your code. Instead we captured the actual outgoing provider payload via a throwaway OMP extension (before_provider_request → JSON dump of event.payload), then ran your own piPreparedStructure + payloadStructure against it plus the session's stored served array from lkg_slot_chunks:
Every role histogram matches exactly, and with the merge removed there are zero positional differences — every role, call count, and result count lines up. On the live session the same shape shows up as the 70-row gap.
Also worth recording, since it invalidates part of our earlier evidence: the "missing texts" we reported (§12§, §36§, …) are an artifact of testing on a fork of the session. The fork re-tags messages with its own tag sequence while the cached openaiResponsesHistory items copied from the original session keep the original tags, so text-prefix matching failed for all 46. Your point that the tags are runtime-only and stored in the MC DB was right, and it is why text matching was the wrong instrument. Roles and IDs are the only things the check compares, and those are what we verified above.
The patch
Diff attached below (17 insertions, 2 deletions), plus two regression tests. A text item only folds into a row that a call item opened; otherwise it starts its own row. This keeps turn boundaries intact and leaves the call-attachment path untouched.
diff --git a/packages/pi-plugin/src/pi-dispatch-structure.ts b/packages/pi-plugin/src/pi-dispatch-structure.ts
--- a/packages/pi-plugin/src/pi-dispatch-structure.ts
+++ b/packages/pi-plugin/src/pi-dispatch-structure.ts
@@ -81,6 +81,11 @@ function payloadStructure(payload: unknown): MessageStructure[] | undefined {
if (!Array.isArray(messages) || messages.length > MAX_MESSAGES)
return undefined;
const rows: MessageStructure[] = [];
+ // Whether the most recent assistant row was opened by a call item rather
+ // than a text item. A text item that follows a call item is the trailing
+ // text of that same turn and folds into it; a text item that follows
+ // another text item is a separate turn and must stay separate.
+ let lastAssistantOpenedByCall = false;
for (const value of messages) {
const message = object(value);
if (!message) return undefined;
@@ -108,6 +113,7 @@ function payloadStructure(payload: unknown): MessageStructure[] | undefined {
if (assistant?.role !== "assistant") {
assistant = { role: "assistant", calls: [], results: [] };
rows.push(assistant);
+ lastAssistantOpenedByCall = true;
}
assistant.calls.push(message.call_id);
continue;
@@ -162,10 +168,19 @@ function payloadStructure(payload: unknown): MessageStructure[] | undefined {
if (
responses &&
row.role === "assistant" &&
- rows.at(-1)?.role === "assistant"
+ rows.at(-1)?.role === "assistant" &&
+ lastAssistantOpenedByCall
) {
+ // Trailing text of the turn the previous call item opened.
rows[rows.length - 1].calls.push(...row.calls);
- } else rows.push(row);
+ } else {
+ // Each responses text item is its own assistant turn. Folding every
+ // consecutive assistant row into the previous one collapses distinct
+ // text-only turns into a single row, so the payload ends up shorter
+ // than the prepared snapshot and the dispatch fence refuses the turn.
+ if (responses && row.role === "assistant") lastAssistantOpenedByCall = false;
+ rows.push(row);
}
}
return rows;
}
Regression tests added to pi-dispatch-structure.test.ts: Responses consecutive text-only turns stay separate rows (fails without the fix, passes with it) and Responses text-only turn after a calling turn stays separate.
Test status
bun test src/pi-dispatch-structure.test.ts → 11 pass, 0 fail (was 9). We also diffed the full src/ suite before and after: the failing set is byte-identical, so no regressions. Those pre-existing failures are module-loading and timing flakes in this sandbox (loadDefaultPiSessionApi, SIGKILL/restart, nudge-golden), unrelated to this file.
We could not push a branch (403, no write access), so the patch is inline above — please apply or tell us and we will reformat however you prefer.
Live result on our side
With the change applied to our local 0.49.1 install, the affected fork's turn no longer fails the structure check; it advances to the next guard (missing current receipt signature), which we believe is also a fork artifact for the tag reason above. We have not yet confirmed on the live session because we did not want to touch it without your go-ahead.
Two asks
Please review/merge this as the 0.49.3 fix (or the leading part of it) so it ships to other hosts.
Confirm the one case we deliberately preserved: a Responses turn whose text is emitted after its call items (our reading of the host builder is that text always precedes calls in a turn, so we treated a text item following a call item as the same turn and folded it). If any host emits the reverse order, tell us and we will key the fold on something stronger than item order.
We can also hand over the throwaway capture extension if it is useful for your own reproduction setup — it is 20 lines and only appends to a file.
Thank you, that's a clean confirmation. Running our own piPreparedStructure and payloadStructure against your captured payload and served array is exactly the check we hoped for, and your counts (1,096 prepared, 1,090 merged, 0 differences without the merge) match what we reproduced on a real Oh My Pi 18.10.4 host. Thanks also for correcting the fork-tag point.
Our fix takes a slightly different route, for one reason: one Oh My Pi assistant message can carry more than one text block, and on the Responses API each block becomes its own message item. If the request side split every text item into its own row, a single assistant turn with a commentary text and an answer text would show up as two rows against one prepared row, and the check would refuse it. So instead the prepared side groups adjacent assistant messages the same way the request side does, and both sides then compare every text item and call id in order. A request that drops one of those texts, or wasn't prepared by Magic Context, is still refused.
That fix is in the 0.49.2 candidate now running CI, together with the leading-system fix, the other provider-API fixes and the fuller doctor --issue report for Oh My Pi. It releases as soon as CI passes. Please don't apply your patch on top of it; 0.49.2 already covers this case.
Fixed in 0.49.2, now on npm (@cortexkit/pi-magic-context@0.49.2). It contains both fixes for this issue:
Leading system entries are now skipped the same way on both sides of the check, so a session whose input starts with one is no longer refused.
On the Responses API, runs of consecutive assistant messages, such as your commentary texts followed by an answer, are grouped the same way on both sides, and every text item is compared.
Your exact session shape (signed commentary, cached Responses history, a lane switch) was refused on a real Oh My Pi 18.10.4 host before the fix and passes with it. A request that drops one of those texts, or that Magic Context didn't prepare, is still refused. Restart Oh My Pi after updating; there's no database migration. If you pinned 0.49.0, you can unpin it now.
Thank you, @Qiiks. Your counts and the payload check against our own functions made this much faster. If a refusal still appears, the message now names the first row that differs, and doctor --issue includes recent errors and the log, so please post that here.
@Qiiks, the image-budget, cached-reply and Codex-replay cases from this thread are all fixed as of 0.49.10. The remaining open case here is the missing current receipt signature refusal from your 0.49.6 capture. Do you still see it on 0.49.10? If you do, please post the refusal lines together with the log lines just before each one. That will show whether those turns ran without Magic Context's history injection.
Yes — still reproducing, on @cortexkit/pi-magic-context@0.49.12 (current newest release). This directly answers the open question above: the missing current receipt signature refusal class still fires on the latest version, and I now have per-occurrence evidence that narrows the remaining root cause further.
Environment
OMP host on Windows; plugin installed under ~/.omp/plugins (@cortexkit/pi-magic-context0.49.12).
Provider openai-codex, model gpt-6.1-sol, session-continuation turns.
Two occurrences on 2026-10-19 21:55:43Z and 21:56:45Z, in different OMP processes (second process freshly started with cwd D:\Coding\eQualizer\edge-extension).
The wrapper trace logs every abort (abort.source) with the calling stack. Both occurrences have the same shape:
hook.enter on a normal dispatch — codex payload keys type,model,input,stream,prompt_cache_key,tools,instructions,store,reasoning,include,client_metadata, no previous_response_id. Turn runs ~5–7 s and completes normally.
End-of-turn internal abort bookkeeping (finish → race → internal cleanup), ~1–2 s after stream completion.
~350–400 ms later: refuse. Stack confirms the plugin dispatch fence (dist/index.js:36121refuse, called from :36254). Following the refusal, the next hook.enter payload carries previous_response_id — a Codex server-side replay of the conversation.
Session aborted by the refusal; a fresh OMP process ran between occurrence 1 and 2.
Reading of the evidence
The refusal text on screen was:
stage=provider dispatch elapsed=352ms recovery=no managed result; refused
(provider request does not match this pass's managed dispatch (missing current receipt signature))
In the 0.49.12 dist, that message is thrown only after the budget has completed and the pass is ready (budget.completed && active.ready), so the context pass itself succeeded — what failed is piHasManagedSignature(payload, active.managedSignature) finding the pass's marker in the outgoing payload.
Hypothesis: on a previous_response_id replay the payload only carries the delta items since the last response (that's the point of server-side replay), so the managed-context marker embedded earlier in history is simply not re-sent. The fence's global signature check can't find it and refuses a dispatch that is actually a legitimate continuation of the managed pass. This would be a distinct false-refuse arm of the weak replacement-based signature check, on the Codex native-replay lane — adjacent to the #714 native-item replay fixes, but on the receipt-verification side rather than the payload-shape side.
The elapsed=352ms is consistent with this being a fast complete-and-dispatch cycle (the budget created at pass start is only ~350 ms old at refusal), not a slow or stalled pass.
To confirm or refute this on the next occurrence, I've installed a diagnostic-only local patch on the live 0.49.12 dist (no behavioral change — the refusal still refuses exactly as before): the signature-miss error now appends a redacted, bounded diagnostic —
provider, model, replay (presence of previous_response_id), inputType, payloadMessages count,
signature=opener(...)/prefix[128] — which marker class the pass embedded (this alone distinguishes the host-owned first-128-chars fallback from a marker-bearing pass),
preparedStructure key summary, payloadHead — roles plus first 48 chars of the first 3 payload messages, newlines flattened.
I'll report back with the diagnostic line after the next occurrence. If it shows replay=true with an opener marker, that's the confirmation; if it shows prefix[128], the host-prefix fallback (RC1) is instead implicated.
Summary for triage
Still firing on 0.49.12 (newest): missing current receipt signature, provider-dispatch stage, fast (elapsed≈350ms) after a normally completed turn.
Evidence pattern suggests the refused payload is a previous_response_id replay missing the re-sent marker.
Local diagnostic patch installed to capture the discriminating evidence on the next occurrence — happy to share the patch diff if useful.
Confirmed on macOS: OMP 18.12.3 + MC 0.49.13, Codex WebSocket delta continuation
Adding concrete local evidence for the previous_response_id hypothesis in the recent follow-up. The same original session now works using OMP's supported SSE configuration, with the dispatch fence still enabled.
Native compaction, native memory and Hide Secrets disabled
Magic Context Doctor: PASS 11 / WARN 0 / FAIL 0
Observed failure
Magic Context's transform and lkgCapture finish successfully. A subsequent provider dispatch refuses with:
provider request does not match this pass's managed dispatch
(missing current receipt signature)
The original private session and request bodies are not attached.
Bounded local evidence
Eight captured pre-change WebSocket continuation requests all had previous_response_id and only 1-5 new input items. None contained the earlier session-history marker.
The full-history requests captured between those continuations contained the managed history marker and could reach the provider.
A plain assistant answer also triggered a failure when OMP's incomplete-todo reminder initiated a continuation. This is not limited to tool calls.
Wiretap's handler only observes the payload and does not replace it. The failure also occurred when Wiretap was temporarily disabled.
Mechanism in the current published host/plugin
OMP buildCodexChainedRequestBody checks the history prefix and replaces full input with appendInput plus previous_response_id.
openCodexWebSocketTransport then invokes options.onPayload with that already-chained frame.
MC piHasManagedSignature searches the first eight outgoing input/message items for a signature taken from the managed context's head. The head is carried by the previous response, so it is absent from the delta frame.
MC refuses and aborts the otherwise-valid continuation.
Set providers.openaiWebsockets to "off" and gracefully restart OMP with --resume pointing to the SAME existing session. No plugin patch, database edit, session replacement, or weakened dispatch fence was used.
Results on the same original session
Two new user messages, five successful provider requests/replies.
Includes one read-only bash tool round trip and two automatic todo-reminder continuations.
All five requests used full SSE payloads without previous_response_id and retained the managed marker.
Zero new context refusals; dispatch_fence=on throughout.
MC status: active tags 248, dropped tags 555, pending marker healthy.
Doctor still PASS 11 / WARN 0 / FAIL 0.
Original session bytes are an unchanged prefix of the appended session file.
Existing plugin entry files, plugin registrations, MC configuration and native compaction/memory/secret settings were preserved.
The current signature predicate correctly finds no marker in that frame, but does not account for the inherited managed prefix.
Requested upstream behavior
Validate the current managed context against the complete effective request before delta projection, or expose an authenticated/correlated host receipt that binds the previous managed prefix and current delta. Preserve fail-closed behavior for unknown, stale, or mismatched previous_response_id and altered prefix/delta content. Simply skipping checks whenever previous_response_id exists would not be a safe fix.
Suggested regression coverage
Original restored session: first full request, then a tool-result delta.
Assistant final answer followed by OMP's automatic todo reminder.
Two consecutive user turns on a reused transport.
History mutation that must break the chain.
Stale/wrong previous_response_id, unknown prefix, and altered content remain refused.
Only redacted counts, public source references and a synthetic request shape are included here; no private transcript, request body, credentials, real response ID or personal filesystem path is attached.
Thank you both, this is exactly the evidence we needed. The missing current receipt signature refusals are confirmed: with Oh My Pi's WebSocket transport on, a chained Codex continuation sends only the new items plus previous_response_id, so the managed history marker the dispatch check looks for isn't in that frame, and a valid continuation is refused.
A fix is in progress for the next patch release. It verifies a chained continuation against the response it continues (the previous_response_id must be the one returned to this session's last managed request, and the new items must match the current pass), and keeps refusing anything it can't verify. As you point out, skipping the check whenever previous_response_id is present wouldn't be safe.
Until it ships, providers.openaiWebsockets: "off" is the workaround @IdealBoy709 verified: full requests keep the check satisfied with no other change.
New refusal class on omp 18.12.3 + pi-magic-context 0.49.13 (anthropic-messages lane, macOS ARM64): a mid-history role: "system" message makes payloadStructure return undefined, so every turn is refused with actualRows=unreadable.
refused (provider request does not match this pass's managed dispatch (prepared turn structure); row=0 expectedRows=171 actualRows=unreadable expectedRole=user actualRole=unreadable expectedCalls=0 actualCalls=unreadable expectedResults=0 actualResults=unreadable comparison=count)
Source of the system row. On session resume, omp 18.12.3 appends a hidden custom_message with customType: "eval-state-context" (display: false), content <system-reminder> Eval runtime state is host-retained, not reconstructed from transcript text. .... It is sent on the wire as role: "system" at its position in history, not at the head.
Captured wire shape (structure only, diagnostic log added just before if (!rows || !expectedRows) in piPreparedStructureMismatch):
index 137 assistant [tool_use], 138 user [tool_result], 139 system[text], 140 assistant [tool_use], 141 user [tool_result]
payloadStructure only skips system/developer while rows.length === 0; at index 139 it falls through to messageStructure, which returns undefined for system, so the whole reader returns undefined.
Timeline. Notice written 09:38:42.667Z, first refusal 09:38:46Z; every turn refused after that, including after two omp restarts. A second session got the same notice and was refused 6 times, then recovered after a restart (presumably the notice left the raw window); not proven.
Workaround. Deleting that single eval-state-context entry from the session JSONL (re-parenting its child) unblocks the session.
Expected fix: model mid-history system rows the way omp serializes them, on both the prepared and the payload side.
Thank you, @tsankotsanev. That's a precise diagnosis. Confirmed: a system row in the middle of the history isn't a row kind the dispatch check knows, so the whole request reads as unreadable and every turn is refused. Oh My Pi adds one on resume (the hidden eval-state-context notice), so an affected session stays refused until that entry leaves the request.
A fix is in progress: mid-history system rows will be modelled the way Oh My Pi sends them, on both sides of the check, while unknown or unprepared rows still refuse. Until it ships, your workaround (removing that one entry from the session file) is the way to unblock a session.
Magic Context 0.49.15 includes the WebSocket fix: Oh My Pi follow-up requests and todo reminders that continue the session's last managed response now pass the dispatch check, so turning off providers.openaiWebsockets is no longer needed. With Hide Secrets on, a follow-up whose new items contain a hidden secret is still refused. The resume case reported here (Oh My Pi 18.12.3 inserting a system note into the middle of the history) is a separate fix that's still in progress, so I'm keeping this issue open for it. Thank you @Qiiks, @Zireael and @IdealBoy709.
0.49.16 also fixes the resume case @IdealBoy709 traced. When Oh My Pi 18.12.3 resumes a session, it inserts a system notice partway through history, and our dispatch check now accepts it, so resumed sessions no longer refuse every turn. The workaround of deleting that entry from the session file isn't needed anymore. Together with 0.49.15's WebSocket continuation fix, that covers every case reported here. If you still see missing current receipt signature on 0.49.16, please reopen with the log line.
However, Oh My Pi's extension runner (packages/coding-agent/src/extensibility/extensions/runner.ts:1718-1721) explicitly strips textSignature whenever an extension edits an assistant text block (e.g. Magic Context prepending its §N§ tag):
(Comment: "A text block with unchanged text keeps its original textSignature even if a handler replaces it; editing text removes its signature because that provider replay state cannot be reused for different text.")
When block.textSignature is stripped by OMP:
signature is undefined.
Line 184 fails on signature?.v !== 1 and aborts replay binding (return texts;).
The fallback texts (line 113) also lacks textSignature, so signaturePhase(block) returns undefined, setting expected.responseTexts to [text, null].
On the wire, OMP replays providerPayload.items directly, which retains the native item's phase: "final_answer" (or "commentary").
payloadStructure receives message.phase from the wire item, setting actual.responseTexts to [text, "final_answer"].
piPreparedStructureMismatch compares [text, null] vs [text, "final_answer"] and refuses the turn with comparison=content at that assistant row.
Fix
In piResponsesAssistantTexts (pi-dispatch-responses-history.ts:184-191), allow binding when textSignature has been stripped by OMP, so long as the text content matches part.text:
This allows replay binding to complete, populating replay with item.phase from the wire item. Both expected.responseTexts and actual.responseTexts then share the same phase ("final_answer" / "commentary"), matching on dispatch.
Verified against session 01a14de4-0e8f-7487-a7aa-1ac02bfc6830 on both affected rows (row with phase: "final_answer" and row with phase: "commentary"): expected and actual responseTexts match exactly.
Thanks @Qiiks, this is a precise trace. It looks like the same root cause as issue 766. Oh My Pi drops a text block's textSignature once we've stripped our tag from it, but still replays the saved provider item, phase included, so our dispatch check expects a different shape than the one sent.
The fix for 766 is in the 0.49.21 patch, which is being assembled now. It accepts Oh My Pi's replay for signature-stripped replies in each of the ways Oh My Pi sends them. We're adding a test with your exact shape (a stripped signature, saved items carrying phase: "final_answer" and "commentary") before release, to confirm it's covered and not just similar.
We won't loosen the signature check itself as your patch proposes, because the check has to keep refusing replies whose signature is present but doesn't match. If 0.49.21 still refuses your session, please post the new log line here.
Second refusal class on cross-model switch (gemini-3.8-flash / Google / Anthropic / Bedrock / Ollama) on 0.49.20:
Error: Magic Context could not safely prepare this turn; send your message again. stage=provider dispatch elapsed=122ms recovery=no managed result; refused (provider request does not match this pass's managed dispatch (prepared turn structure); row=7 expectedRows=104 actualRows=103 expectedRole=assistant actualRole=user expectedCalls=0 actualCalls=0 expectedResults=0 actualResults=0 comparison=role)
Root Cause
When a session history contains an interrupted/aborted assistant turn with only thinking blocks (stopReason: "aborted" | "error" | "length" with no text and no toolCall blocks):
Oh My Pi's shared transformMessages (packages/ai/src/providers/transform-messages.ts:1369) unconditionally drops it before serialization:
Magic Context already models this in piChatHistoryPlan (pi-dispatch-chat-history.ts:52-56) for Chat Completions, but buildPiPreparedStructure (pi-dispatch-structure.ts:348) did not filter it from prepared.messages. Providers that compare against prepared.messages (Google, Anthropic, Bedrock, Ollama) expected the dropped assistant row (104 expected vs 103 wire), failing at row=7 with comparison=role.
Fix
In buildPiPreparedStructure (packages/pi-plugin/src/pi-dispatch-structure.ts:348-353), skip truncated thinking-only assistant messages to match transformMessages:
Verified on session 01a14de4-0e8f-7487-a7aa-1ac02bfc6830: prepared.messages drops from 104 to 103 rows, matching wire actualRows=103 and aligning row 7 with actualRole=user.
Thanks @Qiiks. This one is a separate cause from the Responses phase refusal. You're right about the mechanism: Oh My Pi's transformMessages drops an assistant turn that stopped (length, error or aborted) with only thinking and no text or tool call. Our dispatch projection removes that row only for the Chat Completions and Responses lanes, so on other routes the row counts differ and the turn is refused.
The fix belongs in the same place as the issue 751 fix: mirror the drop exactly where Oh My Pi's providers run transformMessages, and keep the check strict elsewhere. We're deciding whether it goes into the next patch, and we'll post here when it's scheduled. Until then, if a session hits this, starting a new session avoids it.
Two follow-ups from this thread are fixed in 0.49.21:
Phase mismatch on the OpenAI Responses route (@Qiiks): this has the same cause as issue 766. When Oh My Pi drops a text signature after Magic Context strips its tag, it replays the saved item with its phase. The request check now accepts that replay when Magic Context's own tag cleanup, applied to it, gives exactly what Magic Context prepared, and it refuses everything else as before. A test with that exact shape (final_answer and commentary phases) passes on 0.49.21 and is refused on 0.49.20.
Refusal after a model switch: a reply that stopped mid-thinking, with no text and no tool call, is now dropped from the check only on the routes where Oh My Pi itself drops it. That's matched against Oh My Pi's code in each version we test. Every other route sends the same bytes as before. Thanks to @Zireael for the matching test case on issue 761.
Restart Oh My Pi after updating; there's no migration. Thanks, @Qiiks and @Zireael.
Activity
magic-alfonso commentedon Oct 14, 2026
Thank you. Your diagnosis is right, and it's the root cause behind 683 and 684. Pi's history injection prepends
role: "system"messages;piPreparedStructurerejects that role, while the payload reader skips leading system rows. So every session that has injected history gets refused on Oh My Pi, while fresh sessions pass. That's also why our provider tests, which used fresh sessions, missed it.The fix applies the payload side's leading-system skip on the prepared side too, and makes
developeragree on both sides. It adds a regression test for exactly this case, and we're rerunning every provider API on a real Oh My Pi 18.10.4 host with history injected. It's going into 0.49.2 now, together with five other provider-shape fixes, so there's no need for a PR, though thanks for offering. We'll credit you in the release notes. Your local two-line patch is a safe workaround until then.magic-alfonso commentedon Oct 14, 2026
A correction and a question. We checked the published 0.49.1 package against your build: your dist digest matches it exactly. In that build, Magic Context's own history head (m0/m1) is injected as user-role messages, not system. It inserts them after any
role: "system"entry that's already at the head of the session's messages, keeping that entry in place. So the leading system row that trips the check is already in your session's input, and our prepared-side reader rejects it while the request side skips it. Your fix (skip leading system rows on both sides) is still the right one, and it's what 0.49.2 does.To make sure we cover the source of that row, do you know what adds a
system-role entry to your Oh My Pi sessions? For example: another extension, a custom system-prompt or rules setting, or a compaction or summary feature. If you're not sure, the name of any other Oh My Pi extensions you have enabled would help.Qiiks commentedon Oct 14, 2026
Follow-up from live debugging: the leading-system skip (your 0.49.2 fix, which I verified locally) resolves the
!preparedrefusal, but on this host a second divergence remains and still refuses every turn on sessions with injected history.Instrumented
piMatchesPreparedStructureon 0.49.1 + local system-skip, one real OMP 18.10.4 turn, zen/chat-completions lane, ~867-message window:assistant[c0r0]: 58 in payload vs 128 preparedtextSignature(key shapetext+textSignature+type, no other markers); storage shows e.g.textSignature: {v:1, id:"rs_…", phase:"commentary"}on these blocksapi: "openai-responses"and"openai-completions"entries (session moved lanes over its life); some carry cachedproviderPayload: {type: "openaiResponsesHistory", …}So 70 text-bearing, signed assistant messages present in
result.messagesnever reach the chat-completions wire body. Smells like the responses→completions history conversion dropping signed/commentary texts (possibly one of the five provider-shape fixes you mentioned, but flagging in case it isn't covered). Happy to capture any further diagnostics you need — the instrumented build logsSTRUCTDBGcount/hist rows to the magic-context log.Qiiks commentedon Oct 14, 2026
Diagnostic evidence from an isolated fork (session copied to a new ID, tested via
omp -p --resume, live session untouched), with an instrumentedpiMatchesPreparedStructurelogging counts, role histograms, and multiset text diffs (STRUCTDBGlines in the magic-context log):Stable net gaps, same shape. Fork window: payload rows 1089 vs prepared 1095 (gap 6). Live session window: 797 vs 867 (gap 70). Role histograms identical on both sides except
assistant[c0r0](fork: 40 vs 46; live: 58 vs 128). Everything else matches exactly, so this is content-rule loss of one message class, not window drift.The missing messages are exactly the
§N§-tagged assistant texts. Multiset diff of prepared fingerprints against payload fingerprints: every prepared-side fingerprint absent from the payload starts with§<digits>§(e.g.§12§ JWT is live (200)…,§36§ Confirmed against kernel source…, ranges§12§–§647§in the sampled windows). Payload-side assistant items with tags (§10104§…and up) ARE present — so there are two tag series, and only the small-number series vanishes.The small-tag series is runtime-generated, not stored. Grep over the full session JSONL (28k lines, 67MB) finds zero occurrences of the small-tagged texts (e.g.
§12§ JWT is live); they exist only inresult.messages. MC dist contains no code that prepends§N§summaries (only the served-cache validator that recognizes[dropped §N§]/§N§prefixes), so these look like replayed/restored rows (dropped-placeholder restore? fold restore?) injected by the transform pipeline. The large-tag series IS in storage (233 assistant messages starting with§), i.e. tags persisted from prior served turns.Wire side. The zen lane payload is responses-shaped (
body.inputwithfunction_call/function_call_outputitems, 1033 items). The chat converter rule I extracted from the host (text non-blank → keep; drop iff empty && no tool_calls && no reasoning) would keep all 46 texts, so they never reach conversion input — consistent with OMP building the wire list from pre-handler messages while the 46 are handler-added. Untagged-twin check: stripping the§N§prefix from the 46 and searching payload texts finds only 1/40, so they are not present untagged either.Questions for your side: (1) which pipeline stage emits the small-tag
§N§assistant summaries intoresult.messages, and (2) is OMP supposed to forward handler-added messages to the wire, or should the snapshot exclude them? Instrumented build available if you want specific additional fields captured — just say what to log.Qiiks commentedon Oct 14, 2026
Root cause identified — with proof.
result.messageshanded to the snapshot IS MC's own stored last-served array. I read it out of the shared store and compared it against the instrumented snapshot:Instrumented snapshot for the SAME session, SAME turn:
The correspondence is exact —
prepared=867equals the stored array's 867 messages, and the role mapping lines up one-for-one:user+ 137custom+ 40developer+ 1compactionSummaryuser(matches prepareduser[c0r0]=183)toolResulttoolResult✓assistantassistantassistant[c0r0]So the prepared snapshot is taken over MC's LKG-resident served array, while the payload is built by OMP fresh from session entries. OMP's conversion loses 70 of those assistant texts (payload
assistant[c0r0]=58), which is the entire gap. Small-§N§summaries are NOT involved on this session's prepared side (0 in stored array) — earlier log samples showing them came from a different window state.Since
customanddeveloperroles map touserinmessageStructure, the 125 injected guidance blocks survive conversion, but the 128 assistant texts do not survive it — they are present in MC's array and absent from the wire body. That is the mismatch.Why your provider tests miss it: a fresh session has an empty last-served array (no LKG, no
custom/developerinjections, 128 vs 0 assistant-text rows), so prepared == payload and everything passes. Only sessions with MC-managed history hit this.With all evidence attached:
lkg_slots+lkg_slot_chunksextract: stored array = 867 messages, roles{user:5, compactionSummary:1, custom:137, assistant:400, toolResult:284, developer:40}, 77 chunks / 5,011,329 chars, fork equivalent 91 chunks / 5,905,866 charsmessage183,function_call284,function_call_output284,message|assistant282)§N§We can choose between two fixes, and we want your direction:
piPreparedStructuresnapshot the payload side's post-conversion view, or have OMP feed the transform result into the wire body soresult.messagesIS the payload. Either closes the gap for all hosts.piPreparedStructureto tolerate the class of row loss the converter causes (e.g. ignore assistant text-only rows when the payload is responses-shaped). We'd want to know if you see any security/correctness risk in treating that class as "expendable for identity checks", because they're not tool-call/tool-result rows.Please advise which you want us to run. If it is #1, do you want any further captures from the instrumented build — we can log the payload-side pre-conversion message array too, which would nail the exact converter rule dropping the 70.
magic-alfonso commentedon Oct 14, 2026
Thank you, this is exactly the evidence we needed. To your two questions:
§N§prefixes are Magic Context's own tags. Every message part gets one at runtime when the context is prepared, and they're stored only in Magic Context's database, not in the Pi session file. Small numbers are simply older messages. So the tags are expected and not the cause. The higher-numbered tags you found stored in the session file are ones the model copied into its own replies.phase: "commentary"assistant texts and the cachedopenaiResponsesHistoryfrom the lane switch. We're tracing exactly which rule in Oh My Pi 18.10.4 does it, reproducing it on a real host, and matching that rule in the check, without loosening it for anything else.0.49.2 is going out first with the leading-system fix and the other provider-shape fixes, because they're already verified and help other setups. Your session needs this second fix as well, so this issue stays open, and the follow-up comes as 0.49.3. If we need fields from your instrumented build to confirm the rule, we'll ask here.
Qiiks commentedon Oct 14, 2026
Understood and thanks for the precise correction — that resolves the two things we had wrong:
Confirmed:
§N§is MC-runtime tagging, small = older, stored in the MC DB, not the session file. Not the cause. (Our session-file grep for small-tag texts finding nothing is consistent with this: the tags were never in the file at all, and the higher-numbered ones in the file are the model copying them into replies — that also explains why our "untagged twin" multiset check failed.)More importantly: OMP does forward the handler's returned messages, so the lost rows are being dropped by OMP's openai-responses request builder, not by MC. We'll stop treating it as an MC-side snapshot bug and will mirror your 18.10.4 trace.
Two things we can offer to help that trace, since we have both an instrumented build and a reproducer on real hardware:
a) Payload-side pre-conversion capture. We can extend the
STRUCTDBGinstrumentation to log the array as it reaches the responses builder (before item conversion), plus the exact finalpayload.inputitems corresponding to the lost rows. If that would pin the rule faster than your repro, say the word and we'll capture and post it verbatim (redacted if needed — currently logging text prefixes only, ~60 chars, which we can also raise or lower).b) Version pinning. We're on OMP 18.10.4 exactly, single instance, free-tier zen lane,
muse-spark-1.3-contributor-free. If you bisect 18.10.x, we can run the same fork turn against any specific build you name within minutes, since the fork +-pharness is already set up.Regarding the local patch we were about to apply: we're shelving it for now, since your direction says the correct fix is matching OMP's exact drop rule rather than tolerating a class of loss. That's the safer call and we'd rather wait for your 0.49.3 rule match than ship a locally-loosened guard. The instrumented build stays live in the meantime.
Qiiks commentedon Oct 14, 2026
We're on the clock with a real project deadline on the affected session, so we'd like to converge on a patch this hour if possible. Three asks, most specific first:
1. The exact drop rule. If you've traced which rule in OMP 18.10.4's openai-responses builder drops these rows, sharing it lets us mirror it exactly rather than guess. Our best current suspects from reading the host dist, in order:
openaiResponsesHistory→Zhe()/xee()items) rebuilding items only for a subset — e.g. replayingfunction_call/function_call_outputpairs and reconstructing assistant text items only when a round-trip verifies.phase: "commentary"texts being dropped when a model/api pair doesn't acceptthoughtSignature-style metadata on replay (we seetextSignature: {v:1, id:"rs_...", phase:"commentary"}on most of the lost texts, and a few of the survivors have no signature at all).requiresReasoningContentForToolCallsorallowsSyntheticReasoningContentForToolCallsinteracting with reused provider history.2. A blessed local patch shape. Given the deadline, we'd like to apply your approved local patch rather than invent one — our earlier attempt to tolerate "assistant text-only row loss" is exactly the kind of loosening you said you didn't want, and we shelved it. What we'd like to run instead is the mirror-the-rule version: in
piPreparedStructure, apply the same transform you're matching in 0.49.3 to the prepared rows, so the snapshot equals the post-conversion payload. If you can tell us that transform (even pseudocode of the drop condition), we'll implement it against the real code and send you the diff before enabling it, so you can veto.3. Fallback if the rule isn't pinned yet: is there a minimal, vendor-blessed way to make the affected session usable today without the guard change — e.g. a
dispatch_fence/managed_dispatchconfig toggle that defers gracefully (log + send) instead of refusing, so the turn proceeds while we lose the MC guard on that one session only? If such a switch exists in 0.49.1 (even an env var), tell us the name and we'll use it for the deadline, then revert.We can capture and post anything you need from the real host within minutes — including logging the pre-conversion message array that reaches the responses builder, which we haven't instrumented yet but can add in one build.
Qiiks commentedon Oct 14, 2026
Update from the source trace, since you're matching the exact rule. We dug into OMP 18.10.4's dist and found the actual message-dropping rules in the responses request builder. Posting them verbatim so you can confirm/correct rather than re-derive.
Where the drop happens. The responses input is built by
Qc(e.context.messages, e.model, kJn)first (normalization + tool-pairing), then the per-message loop emits items. Three rules can delete whole messages:1.
m3n— truncated/errored empty turns (deletes whole assistant message):Note the second form drops based on the pre-normalization message
Fat the same index.2.
n$i— assistant whose content is entirely aborted tool calls:3. toolResult orphan/stale/duplicate drops (relevant to tool-identity but not to assistant text):
4. Cached-history replay path (when
providerPayload.type==="openaiResponsesHistory"andapi+modelmatch the current model): ifnativeHistory.replay===trueorRQi(items)holds, output is built from the cacheditemsonly —See()scrubs assistant messages whose text is whitespace-only (x3n) andg$i()removes those plus adjacent reasoning; thenjCtis skipped entirely. Non-matching api/model (or no cached payload) falls through tojCt, which does keeptextblocks (it re-derivesmsg_*ids and dropsphase/textSignaturemetadata).Our empirical check against your hypothesis: on our fork, the prepared side's 46 text-only assistant messages are all
stopReason:"stop",model:"muse-spark-1.3-contributor-free", and they all do have a cachedmessageitem containing the text. So rule 4 (with itsx3n/g$iscrub) does not account for them, andm3ncan't fire on them either because they carrytext. Which is why we're asking rather than guessing.Two questions, and then we'll stop poking:
a) Is the 70-row loss entirely explained by
m3n+n$iacting on the session-file entries OMP sends us (our count ofm3ncandidates in the live session's tail window is 21-23 depending on window size, so no, not entirely), or is there a rule we haven't listed?b) If you give us the rule (or a pseudocode predicate over the internal message shape), we'll implement the mirror in
piPreparedStructure, post the diff here for your veto, and only enable it after you sign off. If you'd rather we not touch the guard at all, we'll wait for 0.49.3 — just confirm which, because we have a same-day deadline on this session and need to commit to one path.magic-alfonso commentedon Oct 14, 2026
Thank you for the source trace; it saves us a lot of time, and we've passed it to the engineer reproducing this on a real Oh My Pi 18.10.4 host.
(a) We don't have the full rule yet. Your counts already show that
m3nandn$idon't explain all 70 rows. One thing we're checking first: whether the cached-history replay emits one response's cached items only once when several consecutive assistant messages came from that same response (for example acommentarytext followed by the final answer). That would remove rows while keeping the text, which fits what you saw. Treat that as a hypothesis until we've confirmed it on the host.(b) Please don't patch the check locally. It's what stops Oh My Pi from sending a request Magic Context didn't prepare, and a mirror rule that's slightly too broad would let that through without any sign. For a same-day deadline, the safe option is to pin
@cortexkit/pi-magic-contextto 0.49.0, which doesn't have this check. Its remaining failure mode is the rarer 673/674 refusal, which a resend clears. We'll post the exact rule here as soon as it's confirmed, and the fix ships as 0.49.3.magic-alfonso commentedon Oct 14, 2026
We've confirmed the cause on a real Oh My Pi 18.10.4 host, and it's in Magic Context, not Oh My Pi. Oh My Pi does send all of those assistant texts: the cached-history replay pushes every matching assistant's items (
openai-shared.tspushNativeReplayItems) and doesn't deduplicate them by response. Our check is what lost them. On the responses lane, the request reader merged consecutive assistant items into one row, but the prepared side kept every assistant message. A run of assistant messages, like your commentary texts followed by the answer, therefore looked like missing rows. YourSTRUCTDBGcounts were measuring those merged rows.The fix groups adjacent assistant rows the same way on both sides and keeps every text item. A request that really drops one of those texts, or wasn't prepared by Magic Context, is still refused. Your exact shape (signed commentary, cached responses history, a lane switch) reproduces red on the real host and passes with the fix.
It ships in 0.49.2, together with the leading-system fix; we're holding that release for it. Until then, pinning 0.49.0 remains the safe option. Thanks again: your counts and trace led straight to it.
Qiiks commentedon Oct 14, 2026
We found the root cause, verified it on real data, and have a patch ready. It is not an OMP rule at all — it is MC's own
payloadStructuremerging consecutive assistant rows. Full disclosure of how we got there, because it corrects the direction we pointed you in earlier.The actual bug
packages/pi-plugin/src/pi-dispatch-structure.ts,payloadStructure():For a Responses payload, this folds every assistant row into the previous assistant row when the previous row is also an assistant. Per text item that is fine, but a history containing consecutive text-only assistant turns loses one row per run: two adjacent text-only turns collapse into one row. The prepared snapshot keeps both turns, the payload keeps one, and the fence refuses with
prepared turn structure.The
function_callbranch above it already attaches calls to the preceding assistant row, so this merge only ever fired for text-item-following-text-item — exactly the case where it is wrong.Proof on real data
We instrumented nothing in your code. Instead we captured the actual outgoing provider payload via a throwaway OMP extension (
before_provider_request→ JSON dump ofevent.payload), then ran your ownpiPreparedStructure+payloadStructureagainst it plus the session's stored served array fromlkg_slot_chunks:Every role histogram matches exactly, and with the merge removed there are zero positional differences — every role, call count, and result count lines up. On the live session the same shape shows up as the 70-row gap.
Also worth recording, since it invalidates part of our earlier evidence: the "missing texts" we reported (
§12§,§36§, …) are an artifact of testing on a fork of the session. The fork re-tags messages with its own tag sequence while the cachedopenaiResponsesHistoryitems copied from the original session keep the original tags, so text-prefix matching failed for all 46. Your point that the tags are runtime-only and stored in the MC DB was right, and it is why text matching was the wrong instrument. Roles and IDs are the only things the check compares, and those are what we verified above.The patch
Diff attached below (17 insertions, 2 deletions), plus two regression tests. A text item only folds into a row that a call item opened; otherwise it starts its own row. This keeps turn boundaries intact and leaves the call-attachment path untouched.
Regression tests added to
pi-dispatch-structure.test.ts:Responses consecutive text-only turns stay separate rows(fails without the fix, passes with it) andResponses text-only turn after a calling turn stays separate.Test status
bun test src/pi-dispatch-structure.test.ts→ 11 pass, 0 fail (was 9). We also diffed the fullsrc/suite before and after: the failing set is byte-identical, so no regressions. Those pre-existing failures are module-loading and timing flakes in this sandbox (loadDefaultPiSessionApi, SIGKILL/restart, nudge-golden), unrelated to this file.We could not push a branch (
403, no write access), so the patch is inline above — please apply or tell us and we will reformat however you prefer.Live result on our side
With the change applied to our local 0.49.1 install, the affected fork's turn no longer fails the structure check; it advances to the next guard (
missing current receipt signature), which we believe is also a fork artifact for the tag reason above. We have not yet confirmed on the live session because we did not want to touch it without your go-ahead.Two asks
We can also hand over the throwaway capture extension if it is useful for your own reproduction setup — it is 20 lines and only appends to a file.
magic-alfonso commentedon Oct 14, 2026
Thank you, that's a clean confirmation. Running our own
piPreparedStructureandpayloadStructureagainst your captured payload and served array is exactly the check we hoped for, and your counts (1,096 prepared, 1,090 merged, 0 differences without the merge) match what we reproduced on a real Oh My Pi 18.10.4 host. Thanks also for correcting the fork-tag point.Our fix takes a slightly different route, for one reason: one Oh My Pi assistant message can carry more than one text block, and on the Responses API each block becomes its own
messageitem. If the request side split every text item into its own row, a single assistant turn with a commentary text and an answer text would show up as two rows against one prepared row, and the check would refuse it. So instead the prepared side groups adjacent assistant messages the same way the request side does, and both sides then compare every text item and call id in order. A request that drops one of those texts, or wasn't prepared by Magic Context, is still refused.That fix is in the 0.49.2 candidate now running CI, together with the leading-system fix, the other provider-API fixes and the fuller
doctor --issuereport for Oh My Pi. It releases as soon as CI passes. Please don't apply your patch on top of it; 0.49.2 already covers this case.magic-alfonso commentedon Oct 15, 2026
Fixed in 0.49.2, now on npm (
@cortexkit/pi-magic-context@0.49.2). It contains both fixes for this issue:Your exact session shape (signed commentary, cached Responses history, a lane switch) was refused on a real Oh My Pi 18.10.4 host before the fix and passes with it. A request that drops one of those texts, or that Magic Context didn't prepare, is still refused. Restart Oh My Pi after updating; there's no database migration. If you pinned 0.49.0, you can unpin it now.
Thank you, @Qiiks. Your counts and the payload check against our own functions made this much faster. If a refusal still appears, the message now names the first row that differs, and
doctor --issueincludes recent errors and the log, so please post that here.29 remaining items
Zireael commentedon Oct 17, 2026
Followup edge case logged in #714
magic-alfonso commentedon Oct 17, 2026
@Qiiks, the image-budget, cached-reply and Codex-replay cases from this thread are all fixed as of 0.49.10. The remaining open case here is the
missing current receipt signaturerefusal from your 0.49.6 capture. Do you still see it on 0.49.10? If you do, please post the refusal lines together with the log lines just before each one. That will show whether those turns ran without Magic Context's history injection.Zireael commentedon Oct 19, 2026
Yes — still reproducing, on
@cortexkit/pi-magic-context@0.49.12(current newest release). This directly answers the open question above: themissing current receipt signaturerefusal class still fires on the latest version, and I now have per-occurrence evidence that narrows the remaining root cause further.Environment
~/.omp/plugins(@cortexkit/pi-magic-context0.49.12).openai-codex, modelgpt-6.1-sol, session-continuation turns.D:\Coding\eQualizer\edge-extension).Captured per-occurrence timeline (wrapper
abort.sourcetrace)The wrapper trace logs every abort (
abort.source) with the calling stack. Both occurrences have the same shape:hook.enteron a normal dispatch — codex payload keystype,model,input,stream,prompt_cache_key,tools,instructions,store,reasoning,include,client_metadata, noprevious_response_id. Turn runs ~5–7 s and completes normally.finish→race→ internal cleanup), ~1–2 s after stream completion.dist/index.js:36121refuse, called from:36254). Following the refusal, the nexthook.enterpayload carriesprevious_response_id— a Codex server-side replay of the conversation.Reading of the evidence
The refusal text on screen was:
In the 0.49.12 dist, that message is thrown only after the budget has completed and the pass is ready (
budget.completed && active.ready), so the context pass itself succeeded — what failed ispiHasManagedSignature(payload, active.managedSignature)finding the pass's marker in the outgoing payload.Hypothesis: on a
previous_response_idreplay the payload only carries the delta items since the last response (that's the point of server-side replay), so the managed-context marker embedded earlier in history is simply not re-sent. The fence's global signature check can't find it and refuses a dispatch that is actually a legitimate continuation of the managed pass. This would be a distinct false-refuse arm of the weak replacement-based signature check, on the Codex native-replay lane — adjacent to the #714 native-item replay fixes, but on the receipt-verification side rather than the payload-shape side.The
elapsed=352msis consistent with this being a fast complete-and-dispatch cycle (the budget created at pass start is only ~350 ms old at refusal), not a slow or stalled pass.To confirm or refute this on the next occurrence, I've installed a diagnostic-only local patch on the live 0.49.12 dist (no behavioral change — the refusal still refuses exactly as before): the signature-miss error now appends a redacted, bounded diagnostic —
provider,model,replay(presence ofprevious_response_id),inputType,payloadMessagescount,signature=opener(...)/prefix[128]— which marker class the pass embedded (this alone distinguishes the host-owned first-128-chars fallback from a marker-bearing pass),preparedStructurekey summary,payloadHead— roles plus first 48 chars of the first 3 payload messages, newlines flattened.I'll report back with the diagnostic line after the next occurrence. If it shows
replay=truewith an opener marker, that's the confirmation; if it showsprefix[128], the host-prefix fallback (RC1) is instead implicated.Summary for triage
missing current receipt signature, provider-dispatch stage, fast (elapsed≈350ms) after a normally completed turn.previous_response_idreplay missing the re-sent marker.IdealBoy709 commentedon Oct 20, 2026
Confirmed on macOS: OMP 18.12.3 + MC 0.49.13, Codex WebSocket delta continuation
Adding concrete local evidence for the
previous_response_idhypothesis in the recent follow-up. The same original session now works using OMP's supported SSE configuration, with the dispatch fence still enabled.Environment
Observed failure
Magic Context's transform and lkgCapture finish successfully. A subsequent provider dispatch refuses with:
provider request does not match this pass's managed dispatch
(missing current receipt signature)
The original private session and request bodies are not attached.
Bounded local evidence
Mechanism in the current published host/plugin
Host source references
https://github.com/can1357/oh-my-pi/blob/v18.12.3/packages/ai/src/providers/openai-codex-responses.ts#L3852-L3880
https://github.com/can1357/oh-my-pi/blob/v18.12.3/packages/ai/src/providers/openai-codex-responses.ts#L1944-L1962
Verified supported configuration workaround
Set
providers.openaiWebsocketsto"off"and gracefully restart OMP with --resume pointing to the SAME existing session. No plugin patch, database edit, session replacement, or weakened dispatch fence was used.Results on the same original session
Synthetic shape illustrating the mismatch
Prepared managed messages contain a head such as:
The next valid WebSocket frame can contain only:
The current signature predicate correctly finds no marker in that frame, but does not account for the inherited managed prefix.
Requested upstream behavior
Validate the current managed context against the complete effective request before delta projection, or expose an authenticated/correlated host receipt that binds the previous managed prefix and current delta. Preserve fail-closed behavior for unknown, stale, or mismatched previous_response_id and altered prefix/delta content. Simply skipping checks whenever previous_response_id exists would not be a safe fix.
Suggested regression coverage
Only redacted counts, public source references and a synthetic request shape are included here; no private transcript, request body, credentials, real response ID or personal filesystem path is attached.
magic-alfonso commentedon Oct 20, 2026
Thank you both, this is exactly the evidence we needed. The
missing current receipt signaturerefusals are confirmed: with Oh My Pi's WebSocket transport on, a chained Codex continuation sends only the new items plusprevious_response_id, so the managed history marker the dispatch check looks for isn't in that frame, and a valid continuation is refused.A fix is in progress for the next patch release. It verifies a chained continuation against the response it continues (the
previous_response_idmust be the one returned to this session's last managed request, and the new items must match the current pass), and keeps refusing anything it can't verify. As you point out, skipping the check wheneverprevious_response_idis present wouldn't be safe.Until it ships,
providers.openaiWebsockets: "off"is the workaround @IdealBoy709 verified: full requests keep the check satisfied with no other change.tsankotsanev commentedon Oct 20, 2026
New refusal class on omp 18.12.3 + pi-magic-context 0.49.13 (anthropic-messages lane, macOS ARM64): a mid-history
role: "system"message makespayloadStructurereturn undefined, so every turn is refused withactualRows=unreadable.Source of the system row. On session resume, omp 18.12.3 appends a hidden
custom_messagewithcustomType: "eval-state-context"(display: false), content<system-reminder> Eval runtime state is host-retained, not reconstructed from transcript text. .... It is sent on the wire asrole: "system"at its position in history, not at the head.Captured wire shape (structure only, diagnostic log added just before
if (!rows || !expectedRows)inpiPreparedStructureMismatch):bodyKeys: model, messages, system, tools, metadata, max_tokens, thinking, context_management, output_config, streammessagesLen171,expectedRows172[tool_use], 138 user[tool_result], 139system[text], 140 assistant[tool_use], 141 user[tool_result]payloadStructureonly skipssystem/developerwhilerows.length === 0; at index 139 it falls through tomessageStructure, which returns undefined forsystem, so the whole reader returns undefined.Timeline. Notice written 09:38:42.667Z, first refusal 09:38:46Z; every turn refused after that, including after two omp restarts. A second session got the same notice and was refused 6 times, then recovered after a restart (presumably the notice left the raw window); not proven.
Workaround. Deleting that single
eval-state-contextentry from the session JSONL (re-parenting its child) unblocks the session.Expected fix: model mid-history
systemrows the way omp serializes them, on both the prepared and the payload side.magic-alfonso commentedon Oct 20, 2026
Thank you, @tsankotsanev. That's a precise diagnosis. Confirmed: a
systemrow in the middle of the history isn't a row kind the dispatch check knows, so the whole request reads as unreadable and every turn is refused. Oh My Pi adds one on resume (the hiddeneval-state-contextnotice), so an affected session stays refused until that entry leaves the request.A fix is in progress: mid-history
systemrows will be modelled the way Oh My Pi sends them, on both sides of the check, while unknown or unprepared rows still refuse. Until it ships, your workaround (removing that one entry from the session file) is the way to unblock a session.magic-alfonso commentedon Oct 20, 2026
Magic Context 0.49.15 includes the WebSocket fix: Oh My Pi follow-up requests and todo reminders that continue the session's last managed response now pass the dispatch check, so turning off
providers.openaiWebsocketsis no longer needed. With Hide Secrets on, a follow-up whose new items contain a hidden secret is still refused. The resume case reported here (Oh My Pi 18.12.3 inserting a system note into the middle of the history) is a separate fix that's still in progress, so I'm keeping this issue open for it. Thank you @Qiiks, @Zireael and @IdealBoy709.magic-alfonso commentedon Oct 21, 2026
0.49.16 also fixes the resume case @IdealBoy709 traced. When Oh My Pi 18.12.3 resumes a session, it inserts a system notice partway through history, and our dispatch check now accepts it, so resumed sessions no longer refuse every turn. The workaround of deleting that entry from the session file isn't needed anymore. Together with 0.49.15's WebSocket continuation fix, that covers every case reported here. If you still see
missing current receipt signatureon 0.49.16, please reopen with the log line.Qiiks commentedon Oct 24, 2026
New refusal class on Responses replay (
openai-codex-responses,gpt-6-luna) on 0.49.17 / master:Root Cause
In
piResponsesAssistantTexts(packages/pi-plugin/src/pi-dispatch-responses-history.ts:184-191), replay binding overpayload.itemsrequires:However, Oh My Pi's extension runner (
packages/coding-agent/src/extensibility/extensions/runner.ts:1718-1721) explicitly stripstextSignaturewhenever an extension edits an assistant text block (e.g. Magic Context prepending its§N§tag):(Comment: "A text block with unchanged text keeps its original
textSignatureeven if a handler replaces it; editing text removes its signature because that provider replay state cannot be reused for different text.")When
block.textSignatureis stripped by OMP:signatureisundefined.signature?.v !== 1and aborts replay binding (return texts;).texts(line 113) also lackstextSignature, sosignaturePhase(block)returnsundefined, settingexpected.responseTextsto[text, null].providerPayload.itemsdirectly, which retains the native item'sphase: "final_answer"(or"commentary").payloadStructurereceivesmessage.phasefrom the wire item, settingactual.responseTextsto[text, "final_answer"].piPreparedStructureMismatchcompares[text, null]vs[text, "final_answer"]and refuses the turn withcomparison=contentat that assistant row.Fix
In
piResponsesAssistantTexts(pi-dispatch-responses-history.ts:184-191), allow binding whentextSignaturehas been stripped by OMP, so long as the text content matchespart.text:This allows replay binding to complete, populating
replaywithitem.phasefrom the wire item. Bothexpected.responseTextsandactual.responseTextsthen share the same phase ("final_answer"/"commentary"), matching on dispatch.Verified against session
01a14de4-0e8f-7487-a7aa-1ac02bfc6830on both affected rows (row withphase: "final_answer"and row withphase: "commentary"):expectedandactualresponseTexts match exactly.magic-alfonso commentedon Oct 24, 2026
Thanks @Qiiks, this is a precise trace. It looks like the same root cause as issue 766. Oh My Pi drops a text block's
textSignatureonce we've stripped our tag from it, but still replays the saved provider item, phase included, so our dispatch check expects a different shape than the one sent.The fix for 766 is in the 0.49.21 patch, which is being assembled now. It accepts Oh My Pi's replay for signature-stripped replies in each of the ways Oh My Pi sends them. We're adding a test with your exact shape (a stripped signature, saved items carrying
phase: "final_answer"and"commentary") before release, to confirm it's covered and not just similar.We won't loosen the signature check itself as your patch proposes, because the check has to keep refusing replies whose signature is present but doesn't match. If 0.49.21 still refuses your session, please post the new log line here.
Qiiks commentedon Oct 24, 2026
Second refusal class on cross-model switch (
gemini-3.8-flash/ Google / Anthropic / Bedrock / Ollama) on 0.49.20:Root Cause
When a session history contains an interrupted/aborted assistant turn with only thinking blocks (
stopReason: "aborted" | "error" | "length"with no text and no toolCall blocks):Oh My Pi's shared
transformMessages(packages/ai/src/providers/transform-messages.ts:1369) unconditionally drops it before serialization:where
shouldDropTruncatedThinkingOnlyAssistantis:Magic Context already models this in
piChatHistoryPlan(pi-dispatch-chat-history.ts:52-56) for Chat Completions, butbuildPiPreparedStructure(pi-dispatch-structure.ts:348) did not filter it fromprepared.messages. Providers that compare againstprepared.messages(Google, Anthropic, Bedrock, Ollama) expected the dropped assistant row (104 expected vs 103 wire), failing atrow=7withcomparison=role.Fix
In
buildPiPreparedStructure(packages/pi-plugin/src/pi-dispatch-structure.ts:348-353), skip truncated thinking-only assistant messages to matchtransformMessages:Verified on session
01a14de4-0e8f-7487-a7aa-1ac02bfc6830:prepared.messagesdrops from 104 to 103 rows, matching wireactualRows=103and aligning row 7 withactualRole=user.magic-alfonso commentedon Oct 24, 2026
Thanks @Qiiks. This one is a separate cause from the Responses phase refusal. You're right about the mechanism: Oh My Pi's
transformMessagesdrops an assistant turn that stopped (length,errororaborted) with only thinking and no text or tool call. Our dispatch projection removes that row only for the Chat Completions and Responses lanes, so on other routes the row counts differ and the turn is refused.The fix belongs in the same place as the issue 751 fix: mirror the drop exactly where Oh My Pi's providers run
transformMessages, and keep the check strict elsewhere. We're deciding whether it goes into the next patch, and we'll post here when it's scheduled. Until then, if a session hits this, starting a new session avoids it.magic-alfonso commentedon Oct 25, 2026
Two follow-ups from this thread are fixed in 0.49.21:
final_answerandcommentaryphases) passes on 0.49.21 and is refused on 0.49.20.Restart Oh My Pi after updating; there's no migration. Thanks, @Qiiks and @Zireael.