Skip to content

[Bug] injectMessageIds can double-inject dcp-message-id tags into mid-conversation messages, permanently breaking prompt prefix cache #614

Description

@xiaoqiong0v0

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:

  1. Streaming / incomplete tool parts: while a tool output is still streaming (part not yet completed, part.state.output absent/partial), hasContent() returns false and/or appendToToolPart skips (requires state.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.

  2. Compression block rebuild: syncCompressionBlocks rebuilds message parts. The old injected tag string is not recognized by includes() 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 (msg msg_0553e0aab002mA7RcBIgp6YvFB)
  • prt_05a8d9737001CHpI8eFScL50lF (msg msg_05a8d77fb001SPcwxt0hsozSlC)
  • prt_05a91a376001ROS2pgC3yVkBdb (msg msg_05a917cbc001ckDmoJxCjxfRS5)
  • prt_05a96edae001fwbOBPLZlSjKbO (msg msg_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/running status) — skip injection until completed, then inject exactly once;
  • parts rebuilt by syncCompressionBlocks/compaction — dedupe tags per part so a message never receives two dcp-message-id tags.

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

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