Skip to content

fix: keep DeepSeek Harness sessions on their own cache identity - #890

Merged
kittors merged 2 commits into
devfrom
fix/dsh-session-sticky-header
Aug 14, 2026
Merged

fix: keep DeepSeek Harness sessions on their own cache identity#890
kittors merged 2 commits into
devfrom
fix/dsh-session-sticky-header

Conversation

@kittors

@kittors kittors commented Aug 14, 2026

Copy link
Copy Markdown
Owner

问题

DeepSeek Harness(dsh)经 opencode-go 渠道调用 deepseek-v4-flash 时,上游 prompt 缓存命中率极低且忽高忽低。

根因是 dsh 每轮都发 X-Deepseek-Harness-Session-Id,但不发通用的 Session-Id,body 里也没有 session_id/conversation_id。网关因此完全推导不出它的会话身份,导致两条独立的破坏路径:

  1. requestSessionStickyHeaderKey 不认识该 header → sticky key 为空 → applySessionPromptCacheKey 不注入 prompt_cache_key,同一会话的请求在多个 auth 绑定间漂移。上游前缀缓存是按账号隔离的,漂移即必然 miss。
  2. opencodeGoSessionID 三个来源全部落空(ExecutionSessionMetadataKey 仅 websocket 路径设置),回退到 auth.ID。而 reasoningCacheMaxEntriesPerKey == 1,于是同一 API key 下所有并发会话共用一个 reasoning 槽位,互相覆盖,每轮被注入其它会话的 reasoning_content,请求前缀持续变化。

第 2 条影响更大,且在并发使用 subagent 时被显著放大。

改动

  • session_sticky.go:header 白名单补 X-Deepseek-Harness-Session-Id
  • opencode_go_reasoning.goopencodeGoSessionID 在回退到 auth.ID 前先读 sticky key —— 该 key 本就携带所需的会话身份,无需新增机制。顺带修正空白 metadata 值会短路后续候选的问题。
  • vision/session.goResolveSessionKey 同源问题(header 类客户端拿不到 key,跨轮图片记忆静默失效)。sticky key 在此安全的理由与 auth.ID 不同:它按会话而非按 API key 划分,不会在并发会话间泄漏图片。

三处均保持既有来源的优先级不变,只在原本必然失败的分支上补一条来源。

验证

  • 新增 13 个用例:sticky header 解析 5、opencodeGoSessionID 4、ResolveSessionKey 4。含关键回归:同一 auth 下不同会话必须得到不同 id。
  • 既有用例(含 TestOpenCodeGoSessionID_FallbackToAuthID)无回归。
  • 完整 ./scripts/ci-pr.sh 通过(退出码 0):结构检查、gofmt、secret scan、go vetgo test ./...、golangci-lint v1.64.5、go build

真实命中率需部署后用 dsh 实测 prompt_cache_hit_tokens 确认;本 PR 仅在代码层面证明会话身份已正确传递。

🤖 Generated with Claude Code

kittors and others added 2 commits August 14, 2026 21:44
…dentity

DeepSeek Harness sends X-Deepseek-Harness-Session-Id on every turn, but no
generic Session-Id header and no body session field, so the gateway derived
no session identity for it at all. Two consequences, both of which destroy
the upstream prefix cache:

- requestSessionStickyHeaderKey did not recognize the header, so the sticky
  key stayed empty. applySessionPromptCacheKey then emitted no
  prompt_cache_key, and turns of one conversation drifted across auth
  bindings — upstream prefix caches are scoped per account, so a drifting
  turn is a guaranteed miss.
- opencodeGoSessionID fell through to the auth ID, collapsing every
  concurrent session of one API key onto a single reasoning slot
  (reasoningCacheMaxEntriesPerKey == 1). Sessions overwrote each other and
  were injected with another session's reasoning_content, changing the
  request prefix on every turn.

The sticky key already carries exactly the identity both paths need, so
opencodeGoSessionID now consults it before the auth-ID fallback.

Also stop a present-but-blank metadata value from short-circuiting the
remaining candidates in opencodeGoSessionID.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ResolveSessionKey only consulted ExecutionSessionMetadataKey (websocket-only)
and the generic Session-Id header, so clients that identify sessions with
their own header — DeepSeek Harness, Grok, Codex — resolved no key at all and
silently lost cross-turn image memory.

The sticky key carries the same per-conversation identity and is safe here
for the reason auth.ID is not: it is scoped to one conversation, not to an
API key, so it cannot leak images between concurrent sessions. Existing
sources keep their precedence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kittors
kittors merged commit 719430f into dev Aug 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant