Repository navigation
[Bug] injectMessageIds can double-inject dcp-message-id tags into mid-conversation messages, permanently breaking prompt prefix cache #614
Description
Bug Description
Prompt cache hit rate collapses in long sessions due to duplicate DCP message-id tag injection into mid-conversation messages.
In a single long session (~490K tokens, opencode 1.18.14, @tarquinen/opencode-dcp 3.1.15, dcp.jsonc with maxContextLimit: 800000, minContextLimit: 500000), with no provider/model switching during the affected period, prompt cache hit rate (DeepSeek provider) degraded from normal 95–99% down to ~55% (and at one point 37.9%), then immediately recovered to 96.8%+ after a manual session compact.
Per-step telemetry showed a persistent ~221K-token "never hits cache" segment (cache read stayed pinned near ~272K per call while fresh input stayed ~221K per call), which cannot be explained by normal prefix growth.
Investigation
createChatMessageTransformHandler (hooks.ts) runs on every request before sending to the provider:
stripHallucinations → assignMessageRefs → syncCompressionBlocks → buildToolIdList
→ prune → injectCompressNudges → injectMessageIds → stripStaleMetadata
injectMessageIds (messages/inject/inject.ts) iterates every user/assistant message and appends a \n<dcp-message-id>mNNNN</dcp-message-id> tag to its last text/tool part. Idempotency relies on appendToTextPart/appendToToolPart doing an includes(tag) check before injecting.
Idempotency breaks in two cases:
-
Streaming / incomplete tool parts: while a tool output is still streaming (part not yet
completed,part.state.outputabsent/partial),hasContent()returns false and/orappendToToolPartskips (requiresstate.status === "completed"). The message is then handled via a different branch, and on a later request (once the part completes) the same message is injected again. -
Compression block rebuild:
syncCompressionBlocksrebuilds message parts. The old injected tag string is not recognized byincludes()on the reconstructed part text, so the tag is appended a second time to the same part.
Evidence
Querying the opencode SQLite DB (message/part tables) for session ses_faac2189fffePFTK40ZUh9E71g, 4 parts contain 2 duplicate <dcp-message-id> tags (verified via regex count), all concentrated right before the cache-hit-rate degradation began:
prt_0553e0ac1001JsA5pzr60cfoc8(msgmsg_0553e0aab002mA7RcBIgp6YvFB)prt_05a8d9737001CHpI8eFScL50lF(msgmsg_05a8d77fb001SPcwxt0hsozSlC)prt_05a91a376001ROS2pgC3yVkBdb(msgmsg_05a917cbc001ckDmoJxCjxfRS5)prt_05a96edae001fwbOBPLZlSjKbO(msgmsg_05a96c3af0012HBsvcON9o44RA)
Impact
Providers (esp. DeepSeek-style) cache on byte-exact prefix match. A duplicated tag appended to any mid-conversation message invalidates every cached segment after that message, permanently, until the next compression. Result: ~221K fresh tokens every request → severe token-billing blowup (user observed 25% of daily quota consumed in ~20 minutes).
Expected Behavior
injectMessageIds should be idempotent even for:
- tool parts still streaming (incomplete/
runningstatus) — skip injection until completed, then inject exactly once; - parts rebuilt by
syncCompressionBlocks/compaction — dedupe tags per part so a message never receives twodcp-message-idtags.
Suggestion: make the idempotency check structural (e.g. track injected refs per message in session state keyed by raw part id), not a raw includes() on part text; and guard the injection path so incomplete tool parts don't produce synthetic tags that later get doubled.
Debug Context Logs
DCP has been disabled on the user machine, so fresh debug logs could not be regenerated. The reproducible evidence above comes directly from the opencode DB (message/part tables) via SQL, plus DCP lib/messages/inject/inject.ts + lib/messages/utils.ts source inspection (v3.1.15). Two of the four double-tagged parts' stored text ends with two adjacent <dcp-message-id>...</dcp-message-id> tags, confirming duplicate injection, not a single tag split.
Tool Call Details
Not a compress-tool failure; the issue is the implicit per-request message-id injection (no user-facing tool call involved).
DCP Version
3.1.15
Opencode Version
1.18.14
Model
Other (deepseek-v4-flash via scnet & sensenova-token providers)
Additional Context
- Reproducible pattern: cache hit rate pinned ~55%, "fresh" segment constant ~221K regardless of session growth → not prefix growth; only compression rescues it.
- After removing DCP from the plugin list, the same session class no longer shows the degradation (assessment in progress).
- DCP README already acknowledges ~5% cache-hit cost; this report is about a much larger, non-recoverable hit-rate collapse from duplicated injection, e.g. 85%→55% in our case.
Activity