Repository navigation
[Pi Agent] Conflicting served tool tag numbers permanently block a session, reported as storage busy (0.47.0) #650
Description
Environment
- Host: Pi Agent, not OpenCode or Oh My Pi
- Pi package:
@earendil-works/pi-coding-agent1.1.0 - Magic Context:
@cortexkit/pi-magic-context0.47.0 - OS: macOS
- Node: 24.19.0
codemodeenabled; the affected call was acodemodetool call. Its causal involvement has not been established.
What happened
An existing Pi session became unable to accept further turns. Resending the message repeatedly did not recover it. The UI reported:
Magic Context could not safely prepare this turn; send your message again.
stage=recovery elapsed=53ms recovery=no managed result; refused
(Magic Context storage is busy; send your message again)
However, the underlying logged failure was a tool-tag identity conflict, not an observed SQLite lock:
DEGRADED PASS Conflicting served Pi tool tag numbers; refusing identity adoption:
LKG unavailable (lkg_miss); refusing unreduced 214-message input
The same conflict recurred on retries, leaving the session stuck.
Store observations
Read-only inspection found two type=tool rows in this session for the same tool-call message_id:
| tag_number | tool_owner_message_id | status |
|---|---|---|
| 8 | actual Pi assistant-entry ID | dropped |
| 154 | synthetic fallback owner ID beginning with pi-msg- |
active |
The rows had matching tool-call/token counts. Inspection of the installed code led to the fallback-owner identity adoption path (findPiFallbackToolOwnerTags / adoptPiFallbackToolOwnerTag) and the served-tag conflict check.
This suggests the same tool call acquired a second identity/tag before fallback identity adoption could reconcile it. The initial trigger is unknown; there is no deterministic minimal reproduction yet.
Local recovery
A narrowly scoped transaction removed only duplicate tag 154, retaining original tag 8 and its dropped status. There were no source-content or pending-operation references to the duplicate in the inspected store. The original Pi session JSONL was not modified, and no other session was changed.
Post-transaction checks confirmed a single remaining mapping. Subsequent logs showed progress past the formerly failing preparation step. This is a local workaround, not a proposed general deletion policy: preserving already-served references may require a different fix.
Expected behavior / questions
- Avoid assigning a new served tool tag when an existing mapping for the same call can be identified, or provide a safe supported recovery path when identities conflict.
- Keep the safety checks against serving incorrect/unreduced history, but report an identity conflict rather than describing this as storage contention and recommending an ineffective resend.
- Could fallback-owner tagging and later real-owner adoption produce this state during session restoration or transformation? Is there a supported repair command?
- Is there any known interaction with Pi's
codemode, or is this a general message-identity issue?
Related reports
- [Pi] PiStorageBusyError on SQLite lock contention triggers fail-closed ctx.abort(), blocking interactive turns #601 and [Pi/OMP] A busy-database refusal kills a subagent, because nothing resends its turn (0.45.0) #640 have similar user-visible refusal/recovery symptoms, but concern actual database contention and/or OMP subagents. This report is specifically about a primary Pi Agent session, on 0.47.0, with a logged served-tag identity conflict.
Session IDs, tool-call IDs, local paths, and tool output were intentionally omitted for privacy. No complete session transcript or database is attached.
Activity
magic-alfonso commentedon Oct 10, 2026
Thanks for the careful report and the read-only store inspection; the two rows (tag 8 on the real assistant-entry owner, tag 154 on a
pi-msg-fallback owner, same tool call) are exactly what we need.Two things are clear already:
We're reproducing it now on Pi 1.1.0, including whether session restore or
codemodeplays a part, and looking at how a conflict like this can be repaired safely without breaking tag numbers the model has already seen. Your local repair (removing the duplicate active tag and keeping the original dropped one) matches what we'd expect to be safe in your case, but we'll confirm that before recommending it in general.We'll follow up here with the cause and the fix.
bin115885 commentedon Oct 10, 2026
Follow-up: recurrence with a regular message-tag conflict
The same primary Pi Agent session became blocked again after the earlier tool-tag repair. This time, the logged failure was a message-tag identity conflict rather than a tool-tag conflict:
The preceding pass reported:
Observed duplicate mapping
Two
type=messagerows shared the same nonemptyentry_fingerprint, including the same timestamp, role (user), and content hash::p0pi-msg-0-<timestamp>-user:p0Both
source_contentsrows were present and their content strings were exactly identical (221 characters). The original tag 20 had a pendingdropoperation; duplicate tag 440 had no pending operation.The installed
adoptPiFallbackMessageTagimplementation throws the message-tag error when multiple candidate rows are already inservedTagNumbers. This appears to be another instance of the fallback/real-identity reconciliation failure, now affecting an ordinary user message. A common root cause with the original tool-tag incident is not yet proven.Narrow local workaround
In a single transaction, we removed only tag 440 and its identical
source_contentscopy. We retained tag 20 unchanged, its original content, and all pending operations. Assertions checked row identity/content equality before deletion and confirmed no remaining fallback-versus-real message mapping conflicts for matching fingerprints and part ordinals in this session.The transaction and store checks passed. Successful continuation of the actual Pi conversation after this second repair has not yet been verified. The Pi session JSONL and other sessions were not modified.
This recurrence suggests the problem is not limited to tool results or
codemode. As before, the trigger that first produces the duplicate mapping is unknown. Please let us know whether the message-tag case should be tracked separately.magic-alfonso commentedon Oct 10, 2026
Thanks, this is very useful. It's the same family as the tool-tag case, so we're tracking it here and fixing both together.
What we've found so far, reproduced on real Pi hosts: when an extension earlier in Pi's context chain changes a message's text or the message count without changing its timestamp, Magic Context's owner matching could lose the real Pi entry id. It then gave the same message a second tag under a
pi-msg-stand-in id.codemodetakes the same path as ordinary tools and isn't required to trigger it, though we haven't confirmed what started it in your session.The fix in progress:
It's under review now, and the message-tag case you just hit is being added to it. If it's easy on your side, it would help to know which other Pi extensions run in this session and their load order, especially any that edit, add or remove messages in the context hook.
Zzz210s commentedon Oct 10, 2026
Another Pi host hitting the same family of refusals, with the load-order information @magic-alfonso asked for.
Host: Pi 1.1.0,
@cortexkit/pi-magic-context0.47.0, Windows 11, Node 24.packagesorder in the Pi settings file (Magic Context is 5th):Two of them hook the context path ahead of Magic Context and mutate content:
pi-rtk-optimizertool_result,tool_execution_end,before_agent_startpi-cache-optimizerbefore_agent_start,tool_execution_end,before_provider_headersSymptom here — same user-visible message, different stage than the report above:
Store observations (read-only), in case they narrow it down:
pi-msg-synthetic duplicates anywhere: zero message-fingerprint groups contain a synthetic id. The multi-row tool-owner groups that do exist look like ordinary multiple tool calls owned by one assistant message.context.dbis 701.6 MB with 40.7% free pages; the WAL had grown to 67.9 MB before I checkpointed it;integrity_checkis ok; schema is v95 = supported_fence v95; every running process reports 0.47.0, so there is no mixed-version state.I have not A/B-tested disabling either optimizer yet. Happy to do that and report back if it would help pin the trigger.
magic-alfonso commentedon Oct 10, 2026
Thanks for the details, that helps.
Your log shows a different failure from the one this issue reports, so it's best tracked on its own. There's no identity conflict and no
pi-msg-duplicate. Your turns were refused because:stage=writer admission elapsed=21186ms), while 5 Pi sessions shared onecontext.db;The refusal is the intended safe outcome there, but a 21-second lock wait under 5 sessions shouldn't happen, and "send your message again" isn't helpful advice when it does. Could you open a separate issue with:
writer,lockorbusy, from all five sessions if you can (each Pi process writes its own log);The optimizer A/B test would still help this issue, #650. If disabling
pi-rtk-optimizerorpi-cache-optimizermakes the duplicate-tag refusals stop, that would confirm the trigger we reproduced: an extension that rewrites messages in the context before Magic Context sees them.Zzz210s commentedon Oct 10, 2026
Follow-up: a reproducible instance of an extension rewriting tool output on this host.
pi-rtk-optimizer0.9.0 runs with"mode": "rewrite"andoutputCompaction.enabled = truehere. In this session, a shell tool callreturns the file's frontmatter, then two lines taken from the middle of the same file, then a
[201 more lines]marker — not the six lines that were asked for. The identical request written ashead -n 6 <file> | cat, or asawk 'NR<=6' <file>, returns the correct six lines. So the plainhead -Nform is being rewritten and its output reassembled from elsewhere in the file.Reproduced three times in a row (two different files, same result). I have not yet A/B-tested whether disabling that extension clears the Magic Context refusals — happy to run it and report.
magic-alfonso commentedon Oct 10, 2026
Thanks, that's a clear reproduction. Yes, please run the A/B when you can: the same kind of session with
pi-rtk-optimizerdisabled, and separately with onlypi-cache-optimizerdisabled, and tell us whether the refusals stop. An extension that rewrites tool output, or message content, before Magic Context sees it can make the same tool call look different from one turn to the next, which is the trigger we reproduced for the duplicate-tag refusals. The fix we're preparing makes Magic Context keep the original identity when that happens, instead of refusing the turn.DGVY commentedon Oct 10, 2026
Additional case: Oh My Pi host, no third-party context extension, 4 refusals across 2 concurrent sessions
Thanks for the root-cause explanation. This adds an instance on a different host, plus a reverse-direction observation that may narrow the trigger.
Environment
@cortexkit/pi-magic-context0.47.0 (npm plugin, installed under~/.omp/plugins/)pi-rewind0.5.0 and@cortexkit/pi-magic-context0.47.0 — those two onlyextensionssetting[](empty, no third-party extensions)context.dbSymptom
Four refusals in the log, across two concurrent sessions, all the same message-tag conflict:
Two differences from the reports already on this issue:
lkg_invalidated_reshape, notlkg_miss. That matches the conclusion stated here — "an extension earlier in Pi's context chain … could lose the real Pi entry id".Earlier passes already reported lost stable entry ids
Several passes before the refusal, this session already logged unmapped slots at a stable ~7–8%:
Consistent with the stated root cause: some messages never get a real entry id, the entry-id set drifts between passes, and the LKG prefix check then fails (
lkg_invalidated_reshape).Reverse evidence: no third-party context-rewriting extension on this host
@Zzz210s's report involves
pi-rtk-optimizer/pi-cache-optimizer, which rewrite messages ahead of the context hook. Neither is installed here. OMP'sextensionssetting is empty, and the only other plugin ispi-rewind(checkpoint/rewind). So:If the trigger really is "an earlier extension on the context chain rewrites message text or count", then on this host the source is either OMP itself (how it builds
event.messages, or how it projects branch/compaction entries) or thepi-rewindcheckpoint path. That may help scope the reproduction.Store observations (read-only)
pi-served-tag-numbers/<session>.jsonlis append-only, andgetPiServedTagNumbersreturns the union of every recorded pass, so the set only grows::pNparts of one logical message each hold their own number, and all of them are already in the served set, for example:Among
type=messagerows, the share still inpi-msg-*fallback form: 16/27 in this session, 13/42 in the concurrent one — fallback → real adoption has been incomplete for a long time.Questions
servedRows.length > 1still hold there?~/.omp/plugins/package.jsonand the OMP version, plus more log lines from these sessions, if useful.(Session ids truncated; no full JSONL or database attached.)
magic-alfonso commentedon Oct 10, 2026
Thanks, this is very useful. It fits, and the hosts don't need to be tracked separately.
The trigger isn't third-party extensions as such. It's any message Magic Context can't match back to a saved session entry. Those messages get a temporary
pi-msg-*id, and when the real id turns up on a later pass, the same message can end up with a second tag. Yourresolved=32/37 ... unmapped slots fall through to synthesized idslines are exactly that, and your table (16 of 27 message rows still inpi-msg-*form) shows how often it happened in your session. On your host the unmatched messages can come from Oh My Pi itself, for example messages it builds for the request that aren't saved entries, or frompi-rewind. An extension that rewrites messages is just one more way to get there.Two changes are coming, and both will be in the next release:
lkg_invalidated_reshapecase, Magic Context keeps the newest number for that message, rebuilds the request once, and records why. It never repairs the same message twice. A conflict it can't resolve gets an accurate "tag identity conflict" message instead of "storage is busy". This is in final review.Yes, please send the full list from
~/.omp/plugins/package.jsonand some more log lines. The most useful are thecollectMessageEntryIdsByReflines from the passes just before a refusal, plus anything logged when you usedpi-rewindin those sessions, so we can confirm where the unmatched messages come from.magic-alfonso commentedon Oct 10, 2026
A fix for this is now on master and will ship in the next release.
When one Pi message or tool call ends up with two tag numbers, Magic Context now handles it without getting stuck:
/ctx-statusand the logs), and never repairs the same message twice.The refusal text now says what happened instead of "storage busy". Messages that Pi or another extension adds without a saved session entry also keep their tag numbers when other messages move around them.
We tested it with both setups described here: on Pi 1.1.0 with an extension that rewrites tool output, and on Oh My Pi 18.8.7 with
pi-rewindinstalled and about 8% of messages unmatched. Every turn was served in both.willowite commentedon Oct 11, 2026
Another host for this issue, with a trigger that may help confirm the master fix: Senpi (an OMP/Pi fork,
@code-yeongyu/senpi2026.10.10-11, Bun 1.4.2, Windows 11),@cortexkit/pi-magic-context0.47.0, Magic Context owns compaction. Five refusals in two primary sessions over nine hours, allDEGRADED PASS Conflicting served Pi message tag numbers; refusing identity adoption: LKG unavailable (lkg_invalidated_reshape | lkg_miss).Every refused pass took the positional lane. Of 193 Pi passes, the 184 that logged
collectMessageEntryIdsByRef: resolved=…were all served; the 5 refusals are among the 9 that did not. For each refused pass, the session file has exactly as many emitted entries (message,custom_message,branch_summary) as the pass'smessages=count (41 and 43), soalignedEntryIds?.length === event.messages.lengthheld and positions were trusted.On Senpi they never line up: its
convertToLlmturns custom messages intouserturns, drops some custom types, foldsenvironment-contextinto the next user message, and other extensions inject turns without entries. The counts can still coincide. Store evidence from one session:<router-hint custom entry id>:p0holds the first assistant message (itsresponseIdand timestamp), which tag 7 already holds under its own entry id; tool tags 25-41 are owned by the entry before their real owner. All written by one positional pass.pi-msg-0…pi-msg-4) carry the same five head messages with identicalentry_fingerprint, all served. A fingerprint pass makes thepi-msgtag, a positional pass re-keys it under a wrong real id, the next fingerprint pass makes a newpi-msgtag, and the next positional pass throws.One more data point on the LKG side: the positional pass that wrote tags 24-42 was a forced materialization that also spawned the historian, and its
LKG/served publicationstage took 80.7 s. Its queued LKG capture and ledger line were discarded, but its numbers were already inpi-served-tag-numbers, so the next refusal had no LKG to replay (lkg_invalidated_reshape).Locally we ported the
findPiAlignmentMismatchheader check from master in front of the 0.47.0 positional lane, and made the two "Conflicting served" throws skip the adoption and keep both numbers. With the check, Senpi passes always take the fingerprint lane (custom messages arrive asuser, notcustom). Is a patch release with the #650 fix planned soon? Happy to run it on this host.magic-alfonso commentedon Oct 11, 2026
Thank you, this trace is very useful. It matches what the master fix changes:
userturns and other turns have no entry, that check fails and the pass takes the fingerprint lane, as you saw.Your observation about the forced pass that spent 80.7 s in LKG/served publication, and left its served numbers stored while its last-good capture was discarded, is a separate point. Pi passes are much cheaper on master, but I haven't confirmed that the served numbers and the last-good request are always saved together, so I'm checking that rather than assuming it.
The fix is on master and ships in the next release. We'll post here when it's out, and a run on your Senpi host then would be a good confirmation.
DGVY commentedon Oct 11, 2026
Requested data (OMP host)
1. Plugin list. OMP reads plugins from
~/.omp/plugins/package.json+omp-plugins.lock.json:{ "name": "omp-plugins", "private": true, "dependencies": { "@cortexkit/pi-magic-context": "0.47.0", "pi-rewind": "npm:pi-rewind" } }{ "plugins": { "pi-rewind": { "version": "0.5.0", "enabledFeatures": null, "enabled": true }, "@cortexkit/pi-magic-context": { "version": "0.47.0", "enabledFeatures": null, "enabled": true } }, "settings": {} }OMP's own
extensionssetting is[]. So the context chain ahead of Magic Context holds onlypi-rewind0.5.0 — matching the setup you tested.2. Version correction. When I filed the report the host was OMP 18.8.7; it has since auto-updated to 18.8.9 (
~/.omp/natives/18.8.9/). The refusals below were all on 18.8.7.3.
collectMessageEntryIdsByReflines — caveat. The passes immediately before the four refusals are gone: the per-process log at/tmp/omp/magic-context/magic-context.loghas rotated and now starts at 2026-10-11T16:08Z (~51 KB). What survives are same-shape lines from the same two sessions today, ~4.5% unmapped:For reference, the refusal-time lines I quoted in my first comment were
resolved=32/37,resolved=68/70,resolved=72/75— a similar 7–8% miss rate.4.
pi-rewindlogging — nothing to report. Searching the Magic Context log forrewind,checkpointorrestorereturns no matches in either session, and I found no separatepi-rewindlog directory under~/.omp/. I used no rewind/checkpoint restore during those sessions, so I can't yet confirm whether rewind is the source of the unmatched messages on this host. If you know wherepi-rewindwrites diagnostics, I'll collect from there.5. One observation from the surviving session file. The concurrent session
01a12613-…(the 73-message refusal) still has its JSONL. Its entry types:messagecustomtitle/session/thinking_level_change/service_tier_change/title_changeOnly
messageentries are emit-eligible here (customis notcustom_message), and the file has emitted-entry counts in the same range as the passmessages=counts (156/158/161). That is the shape wherealignedEntryIds?.length === event.messages.lengthcan hold while positions are not trustworthy — the case @willowite describes on Senpi, presumably via OMP's own request construction rather than a rewriting extension.I can attach entry-id/position pairs from that JSONL for the refused pass if that would help confirm it, and I'm happy to re-run on 18.8.9 once the release ships.
(Session ids truncated; no full JSONL or database attached yet.)
magic-alfonso commentedon Oct 11, 2026
Thank you, that's exactly what we needed, and you don't need to send the entry-id pairs.
Your observation matches the other report: a pass where the counts happen to line up while the positions don't. On master, an equal count is no longer enough to trust positions. Each message must also match its saved entry by role, timestamp and call ids, and any mismatch falls back to content matching. With
pi-rewindand no restores used, the 4.5–8% unmatched slots on your host are most likely messages Oh My Pi builds for the request that have no savedmessageentry. On master those are matched by content and don't break the session.We'll post here when the release with the fix is out. A run on 18.8.9 then would be a good confirmation.
magic-alfonso commentedon Oct 12, 2026
Fixed in 0.48.0. A conflicting served tag number now resolves without permanently blocking the session: Magic Context keeps the number the model already saw where it can prove it, otherwise performs one recorded rebuild, and never refuses the same conflict more than one turn. The refusal no longer says 'storage busy' for this case. Pi also now saves the served tag numbers and the last good request together, so a slow pass can't leave one without the other. Thanks to everyone who traced this.