Skip to content

pi dispatch fence: piPreparedStructure rejects MC's own leading system injection (m0/m1), refusing every injected turn #686

Closed

Description

@Qiiks

Summary

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-context 0.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)
  • Harness: OMP (@oh-my-pi/pi-coding-agent 18.10.4), dispatch fence enabled
  • DB healthy (integrity_check: ok, rebuilt from salvage)

Mechanism (traced in dist/index.js)

  1. 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)
  2. The context handler then snapshots: preparedStructure = piPreparedStructure(result.messages).
  3. 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.
  4. Meanwhile payloadStructure explicitly skips leading system/developer rows, so the payload side is well-defined.
  5. The dispatch check treats !prepared as mismatch → refusal. Signature check passes (managed content is present); only the structure snapshot is broken.

Failing-turn log sequence:

transform completed in 3886.4ms (870 messages, 724 targets, watermark: 10719)
CONTEXT REFUSED "... stage=provider dispatch elapsed=5991ms ... (prepared turn structure)"

Suggested fix

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):

if (rows.length === 0 && (source?.role === "system" || source?.role === "developer"))
  continue;

Happy to PR this if you want it from outside.

Workaround used

Patched the installed dist/index.js (2 lines, backup kept); turns on the previously-failing 35k-message session dispatch normally again.

Activity

  1. magic-alfonso commented on Oct 14, 2026

    @magic-alfonso

    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.

  2. magic-alfonso commented on Oct 14, 2026

    @magic-alfonso

    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.

  3. Qiiks commented on Oct 14, 2026

    @Qiiks
    ContributorAuthor

    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.

  4. Qiiks commented on Oct 14, 2026

    @Qiiks
    ContributorAuthor

    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.

  5. Qiiks commented on Oct 14, 2026

    @Qiiks
    ContributorAuthor

    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:

    lkg_slots row (live session 01a0c1d0): json_prefix_chars=5011329, chunks=77, model_key=opencode-zen/muse-spark-1.3-contributor-free, captured_at=1792012971242
    stored last-served array: 867 messages, roles={user:5, compactionSummary:1, custom:137, assistant:400, toolResult:284, developer:40}, small_tag_summaries=0, big_tag_summaries=571
    

    Instrumented snapshot for the SAME session, SAME turn:

    STRUCTDBG count rows=797 prepared=867
    rowsHist  {user:183, assistant[c1r0]:264, toolResult:284, assistant[c2r0]:5, assistant[c3r0]:2, assistant[c4r0]:1, assistant[c0r0]:58}
    prepHist  {user:183, assistant[c0r0]:128, assistant[c1r0]:264, toolResult:284, assistant[c2r0]:5, assistant[c3r0]:2, assistant[c4r0]:1}
    

    The correspondence is exact — prepared=867 equals the stored array's 867 messages, and the role mapping lines up one-for-one:

    Stored array → prepared rows
    5 user + 137 custom + 40 developer + 1 compactionSummary 183 user (matches prepared user[c0r0]=183)
    284 toolResult 284 toolResult ✓
    272 call-bearing assistant 272 call-bearing ✓
    128 text-only assistant 128 assistant[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 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.

    With all evidence attached:

    • lkg_slots + lkg_slot_chunks extract: 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 chars
    • live-wire payload shape: responses API, 1033 items (message 183, function_call 284, function_call_output 284, message|assistant 282)
    • multiset diff: 80 prepared-side assistant texts absent from payload, all tagged §N§

    We can choose between two fixes, and we want your direction:

    1. 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.
    2. 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.

  6. magic-alfonso commented on Oct 14, 2026

    @magic-alfonso

    Thank you, this is exactly the evidence we needed. To your two questions:

    1. 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.
    2. 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.

  7. Qiiks commented on Oct 14, 2026

    @Qiiks
    ContributorAuthor

    Understood and thanks for the precise correction — that resolves the two things we had wrong:

    1. 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.)

    2. 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.

  8. Qiiks commented on Oct 14, 2026

    @Qiiks
    ContributorAuthor

    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.

  9. Qiiks commented on Oct 14, 2026

    @Qiiks
    ContributorAuthor

    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):

    function m3n(e){return(e.stopReason==="length"||e.stopReason==="error"||e.stopReason==="aborted")&&!e.content.some((s)=>s.type==="toolCall"||s.type==="text")}
    ...
    if(F.role==="assistant"&&m3n(F))continue;

    Note the second form drops based on the pre-normalization message F at the same index.

    2. n$i — assistant whose content is entirely aborted tool calls:

    if(i.type==="toolCall"){let a=c3n(i),l=s.get(i.id);...;if(a)continue;r.push(i)}
    ...
    if(r.length===0)continue;                     // whole assistant message dropped

    3. toolResult orphan/stale/duplicate drops (relevant to tool-identity but not to assistant text):

    if(x.has(_))continue;                          // already consumed
    if(!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 items only — 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.

  10. magic-alfonso commented on Oct 14, 2026

    @magic-alfonso

    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.

  11. magic-alfonso commented on Oct 14, 2026

    @magic-alfonso

    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.ts pushNativeReplayItems) 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.

  12. Qiiks commented on Oct 14, 2026

    @Qiiks
    ContributorAuthor

    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.

    The actual bug

    packages/pi-plugin/src/pi-dispatch-structure.ts, payloadStructure():

    if (
        responses &&
        row.role === "assistant" &&
        rows.at(-1)?.role === "assistant"
    ) {
        rows[rows.length - 1].calls.push(...row.calls);
    } else rows.push(row);

    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:

    prepared rows: 1096
    payload WITH merge    : 1090     (gap 6)
    payload WITHOUT merge : 1096     (gap 0)
    
    prepared hist : {user:190, assistant[c1r0]:407, toolResult:440, assistant[c2r0]:7, assistant[c0r0]:46, assistant[c3r0]:5, assistant[c4r0]:1}
    noMerge  hist : {user:190, assistant[c1r0]:407, toolResult:440, assistant[c2r0]:7, assistant[c0r0]:46, assistant[c3r0]:5, assistant[c4r0]:1}
    positional diffs WITHOUT merge: 0
    

    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

    1. Please review/merge this as the 0.49.3 fix (or the leading part of it) so it ships to other hosts.
    2. 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.

  13. magic-alfonso commented on Oct 14, 2026

    @magic-alfonso

    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.

  14. magic-alfonso commented on Oct 15, 2026

    @magic-alfonso

    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.

  15. 29 remaining items

  16. Zireael commented on Oct 17, 2026

    @Zireael
    Contributor

    Followup edge case logged in #714

  17. magic-alfonso commented on Oct 17, 2026

    @magic-alfonso

    @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.

  18. Zireael commented on Oct 19, 2026

    @Zireael
    Contributor

    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-context 0.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).

    Captured per-occurrence timeline (wrapper abort.source trace)

    The wrapper trace logs every abort (abort.source) with the calling stack. Both occurrences have the same shape:

    1. 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.
    2. End-of-turn internal abort bookkeeping (finish → race → internal cleanup), ~1–2 s after stream completion.
    3. ~350–400 ms later: refuse. Stack confirms the plugin dispatch fence (dist/index.js:36121 refuse, called from :36254). Following the refusal, the next hook.enter payload carries previous_response_id — a Codex server-side replay of the conversation.
    4. 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.
  19. IdealBoy709 commented on Oct 20, 2026

    @IdealBoy709

    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.

    Environment

    • macOS ARM64
    • OMP 18.12.3, compiled Homebrew executable
    • @cortexkit/pi-magic-context 0.49.13
    • Provider: openai-codex; model: gpt-6.1-sol; API: openai-codex-responses
    • Long-lived session restored in Orca
    • 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

    1. OMP buildCodexChainedRequestBody checks the history prefix and replaces full input with appendInput plus previous_response_id.
    2. openCodexWebSocketTransport then invokes options.onPayload with that already-chained frame.
    3. OMP's onPayload bridge emits before_provider_request.
    4. 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.
    5. MC refuses and aborts the otherwise-valid continuation.

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

    Synthetic shape illustrating the mismatch

    Prepared managed messages contain a head such as:

      [{ role: "user", content: "<session-history>synthetic fixture</session-history>" }, ...]

    The next valid WebSocket frame can contain only:

      {
        type: "response.create",
        previous_response_id: "synthetic-prior-response",
        input: [{ type: "function_call_output", call_id: "synthetic-call", output: "synthetic-result" }]
      }

    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.

  20. magic-alfonso commented on Oct 20, 2026

    @magic-alfonso

    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.

  21. tsankotsanev commented on Oct 20, 2026

    @tsankotsanev
    Contributor

    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):

    • bodyKeys: model, messages, system, tools, metadata, max_tokens, thinking, context_management, output_config, stream
    • messagesLen 171, expectedRows 172
    • 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.

  22. magic-alfonso commented on Oct 20, 2026

    @magic-alfonso

    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.

  23. magic-alfonso commented on Oct 20, 2026

    @magic-alfonso

    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.

  24. magic-alfonso commented on Oct 21, 2026

    @magic-alfonso

    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.

  25. Qiiks commented on Oct 24, 2026

    @Qiiks
    ContributorAuthor

    New refusal class on Responses replay (openai-codex-responses, gpt-6-luna) on 0.49.17 / master:

    Error: Magic Context could not safely prepare this turn; send your message again. stage=provider dispatch elapsed=1343ms recovery=no managed result; refused (provider request does not match this pass's managed dispatch (prepared turn structure); row=23 expectedRows=142 actualRows=142 expectedRole=assistant actualRole=assistant expectedCalls=0 actualCalls=0 expectedResults=0 actualResults=0 comparison=content)
    

    Root Cause

    In piResponsesAssistantTexts (packages/pi-plugin/src/pi-dispatch-responses-history.ts:184-191), replay binding over payload.items requires:

    if (
        signature?.v !== 1 ||
        typeof signature.id !== "string" ||
        item.id !== signature.id ||
        item.phase !== signature.phase ||
        part.text !== (block.text as string).replace(/^§\d+§ /u, "")
    )
        return texts;

    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):

    if (block.type !== "text" || !isRewritten(index)) return block;
    const { textSignature: _stale, ...rest } = block;
    return { ...rest, text: texts[index] as string };

    (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:

    1. signature is undefined.
    2. Line 184 fails on signature?.v !== 1 and aborts replay binding (return texts;).
    3. The fallback texts (line 113) also lacks textSignature, so signaturePhase(block) returns undefined, setting expected.responseTexts to [text, null].
    4. On the wire, OMP replays providerPayload.items directly, which retains the native item's phase: "final_answer" (or "commentary").
    5. payloadStructure receives message.phase from the wire item, setting actual.responseTexts to [text, "final_answer"].
    6. 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:

    const cleanText = (block.text as string).replace(/^§\d+§ /u, "");
    if (
        (signature &&
            (signature.v !== 1 ||
                typeof signature.id !== "string" ||
                item.id !== signature.id ||
                (signature.phase !== undefined && item.phase !== signature.phase))) ||
        (part.text !== cleanText && part.text !== block.text)
    )
        return texts;

    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.

  26. magic-alfonso commented on Oct 24, 2026

    @magic-alfonso

    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.

  27. Qiiks commented on Oct 24, 2026

    @Qiiks
    ContributorAuthor

    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:

    if (originalMsg.role === "assistant" && shouldDropTruncatedThinkingOnlyAssistant(originalMsg)) {
        continue;
    }

    where shouldDropTruncatedThinkingOnlyAssistant is:

    export function shouldDropTruncatedThinkingOnlyAssistant(msg: AssistantMessage): boolean {
        const isTruncatedStop = msg.stopReason === "length" || msg.stopReason === "error" || msg.stopReason === "aborted";
        return isTruncatedStop && !msg.content.some(block => block.type === "toolCall" || block.type === "text");
    }

    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:

    if (
        source?.role === "assistant" &&
        ["length", "error", "aborted"].includes(String(source.stopReason)) &&
        (!Array.isArray(source.content) ||
            !source.content.some(
                (block) =>
                    object(block)?.type === "text" ||
                    object(block)?.type === "toolCall",
            ))
    )
        continue;

    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.

  28. magic-alfonso commented on Oct 24, 2026

    @magic-alfonso

    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.

  29. magic-alfonso commented on Oct 25, 2026

    @magic-alfonso

    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.

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