Repository navigation
bug(omp): chained continuation refused after a compaction-marker drain — synthetic m[0]/m[1] timestamps are part of the chain prefix (0.49.22) #791
Description
Short description
On OMP, a chained continuation is refused right after a compaction-marker drain with
chained continuation: prepared history does not extend the previous managed request, although the only difference
between the two prepared histories is the timestamp on the plugin's own injected m[0]/m[1] pair. Those
timestamps are derived from the current head (head.timestamp - 2 / - 1), the drain removes that head, and
piPreparedHistory() includes timestamps in the positional prefix comparison.
Reproduced with the shipped functions and the two real session entries: prefix 0 of 4 as-is, 4 of 4 with the
synthetic timestamps normalized. The turn is lost and resending reproduces it.
What happened
Plugin 0.49.22, OMP 18.12.6, openai-codex-responses / gpt-6.1-sol, Windows x64.
07:32:06.287 Pi compaction-marker drain: persisted compactionId=9a82e9d0 firstKept=c630be31 …
07:32:06.289 Pi compaction-marker clear: reason=applied endMessageId=synth-user-75a6478d ordinal=3135
07:32:06.519 injected m[0]/m[1] into Pi messages (97534 + 2407 bytes, materialized=false)
07:32:36.544 tail hygiene prefix invalidated: part_index=0 message=timestamp:1792913034834:user
field=key frozen_parts=338 measured_parts=343 action=re-measured generation=8
07:32:36.662 [magic-context][pi] CONTEXT REFUSED "Magic Context could not safely prepare this turn; send your
message again. stage=provider dispatch elapsed=315ms recovery=no managed result; refused
(provider request does not match this pass's managed dispatch (chained continuation:
prepared history does not extend the previous managed request))"
A second occurrence at 2026-10-26 07:14:52.076Z has the same shape (firstKept=f98a2411, part_index=0 message=timestamp:1792912401145:user).
Note the tail-hygiene line: the plugin's own prefix cache is also invalidated by the same move, which is why every
post-drain pass re-measures from scratch.
Root cause
-
prependM0M1Messages()(packages/pi-plugin/src/inject-compartments-pi.ts:2649) timestamps the injected pair from
the current first visible message:const baseTimestamp = timestampHint ?? piMessages[0]?.timestamp ?? Date.now(); … { role: "user", content: m0Content, timestamp: baseTimestamp - 2 }, { role: "user", content: [{ type: "text", text: m1 }], timestamp: baseTimestamp - 1 }
Both call sites derive it from the live head (
…:2726passesmessages[0]?.timestamp). -
piPreparedHistory()(pi-dispatch-chain.ts) snapshots every prepared message withlkgContentFields(), which
flattens all enumerable fields,timestampincluded (packages/plugin/src/hooks/magic-context/lkg-slot.ts:260). -
The drain removes the head those timestamps were derived from, so the next pass re-injects the pair with new
timestamps. Content is byte-identical otherwise; only the two synthetic numbers move.
exactReusablePrefix(prepared.snapshots, anchor.prepared.snapshots)returns 0 andpiExpandChainedRequest()
refuses atpi-dispatch-chain.ts:215.
Evidence
reports/dispatch-failures/m0-timestamp-probe.mjs runs the shipped prependM0M1Messages, piPreparedHistory,
lkgContentFields and exactReusablePrefix against the real entries named by the log
(pre-drain 09e940de, post-drain f98a2411):
| Measurement | Value |
|---|---|
| pre-drain head timestamp | 1792912401147 |
| post-drain head timestamp | 1792912401580 |
injected m[0] timestamp before → after |
1792912401145 → 1792912401578 (+433 ms) |
exactReusablePrefix as-is |
0 of 4 (refuses) |
exactReusablePrefix, synthetic timestamps normalized |
4 of 4 (admits) |
The probe also asserts that the rest of the two histories is identical and that the injected m[0] text did not
change.
Proposed fix
- Give the injected pair a timestamp that is a function of the session, not of the current head: persist the
base next to them0/m1cache row and pass it through the existingtimestampHintparameter. This also stops the
tail-hygiene re-measure that the log shows on every post-drain pass. - Belt and braces: the two injected rows carry no entry id. Exclude their
timestampfrom chain identity (they are
plugin-owned text, never provider-visible content), keeping every other field compared. - Regression test: anchor a pass → drain trims the head → next pass on a chain-capable model must be admitted. This
is a chain-path test, not a structural one: the prepared content never changed, so a refusal is a false positive
by construction.
Environment
@cortexkit/pi-magic-context0.49.22 (present in master0.49.23:prependM0M1Messagesand
piPreparedHistoryare unchanged)- OMP 18.12.6, Bun, Windows x64
openai-codex-responses,gpt-6.1-sol, WebSocket chain enabled (previous_response_idframes observed)- Session
01a1584d…, 761 messages, compaction markers drained repeatedly - Related: Design proposal: tier the dispatch verdict so unmodeled shapes degrade the check, not the turn #761 (verdict tiering — this is one concrete shape with a proven fix inside the current design)
Activity
magic-alfonso commentedon Oct 26, 2026
Thanks, and for the probe that isolates the timestamp move.
Confirmed. The injected
m[0]/m[1]pair takes its timestamps from the current first message (prependM0M1Messages), the drain removes that message, and the chain prefix comparison includes the timestamps. So two histories with identical content compare as different, and the continuation is refused.We're working on a fix that keeps the injected pair's timestamps stable across a drain. I'll post here when it ships.
magic-alfonso commentedon Oct 27, 2026
Fixed in 0.49.24. Thanks @Zireael for the diagnosis.
After a summary is folded into the history, the injected history block gets new timestamps. The chained-request check now ignores the timestamp only on that injected pair, matched by the injector's exact shape, so chained continuations are no longer refused there. A changed timestamp on a real user message is still refused, and no bytes sent to the model changed.
Update the Pi package and restart Oh My Pi to pick it up.