Skip to content

feat: session id is the client-provided conversation value verbatim; 400 without identity (#286) - #288

Merged
ranxianglei merged 5 commits into
masterfrom
2026-08-27_strong-signal-session-id
Aug 27, 2026
Merged

feat: session id is the client-provided conversation value verbatim; 400 without identity (#286)#288
ranxianglei merged 5 commits into
masterfrom
2026-08-27_strong-signal-session-id

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Aug 27, 2026

Copy link
Copy Markdown
Owner

#286:session ID = client-provided conversation 值原文(无 hash、无其他维度)

设计(owner 确认)

  • session ID = client-provided conversation 值原文:codex session-id/thread-id、claude x-claude-code-session-id、opencode x-opencode-session、Responses body session_id / metadata.session_idprompt_cache_key 等。
  • 无强信号 → 400(错误信息列出可用身份字段);content-fingerprint 会话禁用(指纹有真实碰撞面,静默建会话只会孤儿化状态)。
  • credential(OAuth bearer 轮换)、upstream(中转切换)、protocol(中转翻译 / 跨协议切模型)都是可变的,均不参与身份。会话状态本身协议中立(kernel CompressionState + 文本 blocks + CoreMessage),客户端每轮重发全量历史,协议切换不断状态。
  • meta.protocol / upstreamOrigin 保持 first-wins(持久化文件名命名空间 sessions/<proto>/<host>_<sha>.json 由此派生,更新会孤儿化旧文件);persist.ts 无需改动(文件名已是 id 的 sha256,id 从文件体读取)。

变更

  • src/session-id.ts:删除 deriveSessionId / extractKey;保留 clientConversationHeader / affinityToken / preferPromptCacheKeyIdentity
  • src/server.ts
    • 无强信号 → 400(anthropic 与 openai/responses 各自的错误格式)。
    • sessionId = conversation(原文)。
    • 修复本设计暴露的潜在 bug:route 的 compressProtocol 配置原先按 session.meta.upstreamOrigin(first-wins)解析——切换中转后新中转的配置会被静默忽略;现按请求实际 upstream 解析。
    • debug dump 文件名 sanitize(safeSessionId)——id 现在是客户端原始值。
  • 测试:e2e-session-identity.test.ts 重写(10 例:bearer 轮换 / 中转切换 / 跨协议续接 / 匿名 400 ×3 / 隔离);7 处匿名测试调用点补强信号;2 个 headless-binding 测试首请求带与 register 不同的强信号(保持走 pending 路径)。

代价

  • 一次性:存量持久化会话重新定键(旧记录孤儿化)——可接受(owner 确认)。
  • 无原生 session id 的客户端(hermes/dsh 无 plugin、通用客户端)现在会收到 400——显式失败,而非静默丢状态。

验证

  • typecheck clean;657/657 tests pass。

…entity (#286)

Strong-signal conversation identities (codex session-id/thread-id,
claude x-claude-code-session-id, opencode x-opencode-session,
Responses body session_id, prompt_cache_key) now derive the session id
from hash(protocol|conversation) only. The 4-dim hash
(protocol|upstream|credential|conversation) is retained for weak
signals (content fingerprints), where the credential/upstream
dimensions provide collision isolation.

Rationale: the credential dimension is a rotating OAuth bearer for
Codex/ChatGPT and the upstream dimension changes when users switch
relays — either change orphaned all compression state mid-session
(#280). The client's native session id is the only invariant bound to
the conversation.

One-time cost: existing persisted sessions are re-keyed once (old
records orphaned), accepted per issue discussion.
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

📦 Built Package Artifact

Branch: 2026-08-27_strong-signal-session-id (950c89b)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-288

Each push to this PR publishes a new version under the pr-288 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr288.tgz
npm install -g package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · vllm-qwen/qwen3.8-27b

[bot] Review of PR #288 (branch 2026-08-27_strong-signal-session-id, commit 6215446) — LGTM, ready for human merge.

Verified locally (reproduced CI checks on the branch)

  • npm run typecheck — clean
  • npm test670/670 pass (incl. the 6 new strong-tier unit tests in tests/proxy-session-id.test.ts and all 7 tests in new tests/e2e-session-identity.test.ts, which I also ran in isolation: 7/7)
  • npm run build — success

Code-level verification

  • Kernel semantics match the PR description. acp-kernel 0.0.46 conversationIdentityResponses: header / body session_id / metadata.session_idclientProvided: true; previous_response_id / content-fingerprint → false. Proxy-side preferPromptCacheKeyIdentity promotes fingerprint→pck to true exactly when source === "content-fingerprint", so it can never override a stronger signal.
  • Flag computation per protocol in src/server.ts:760-769 is correct:
    • responses: responsesIdentity?.clientProvided ?? false — identity always exists, tier matches the conversation value source.
    • openai: openaiIdentity?.clientProvided ?? false — header → true, fingerprint → false, pck promotion → true.
    • anthropic: !!convHeaderconvHeader = clientConv ?? sessionHeader, both are client-sent request headers, so true ⟺ the client actually provided a conversation signal.
  • Single session-derivation site (src/server.ts:770 deriveProxySessionId); all callers of the changed deriveSessionId signature are updated; no other getSession/session-creation paths bypass the flag.
  • Weak tier unchanged: fingerprint sessions still hash protocol|upstream|credential|conversation; the e2e control test confirms same-content cross-credential isolation is preserved (3 sessions for [no-auth ×2, keyX ×1, different-content ×1]).
  • AGENTS.md compliance: no as any / @ts-ignore / console.* in the diff; version untouched; branch naming + feat: commit convention OK.
  • One-time re-key cost: persist layer is lazy per-key, so orphaned old-keyed records are inert (never loaded); matches the accepted trade-off in 会话身份不稳定:credential/upstream 维度导致长会话状态孤儿化(sessions 缺失) #286.

Non-blocking notes

  1. Tier flip: a client that intermittently sends/omits its conversation header will create two sessions (strong + weak) for one conversation. The "tiers never collide" test documents this as intended — no silent state migration. Fine.
  2. Strong tier drops the credential dimension, so two different accounts on one proxy instance sharing an identical client-provided conversation id would share compression state. In practice the ids in scope are UUIDs (codex thread-id, claude session-id, opencode session) — negligible collision surface, and the proxy is single-user local. Accepted in 会话身份不稳定:credential/upstream 维度导致长会话状态孤儿化(sessions 缺失) #286 discussion; noting for the record.
  3. previous_response_id-only clients remain weak tier, so their session id still churns per turn — pre-existing behavior, not a regression, out of scope here.

Coordination

Confirmed: #249's chatgpt-account-id credential-dimension part is superseded by this fix (account switches must not break session continuity); thread-id subagent identity + compaction fast path remain complementary. #281/#283 unaffected.

Per repo rules I will not merge — please merge manually: #288

@ranxianglei ranxianglei changed the title feat: key sessions on (protocol, conversation) for client-provided identity (#286) feat: session id is the client-provided conversation value verbatim; 400 without identity (#286) Aug 27, 2026
ework-agent and others added 3 commits August 27, 2026 19:27
- dsh: force PI_CACHE_RETENTION=long so pi-ai stamps prompt_cache_key
  (the dsh session uuid) on every request; the prompt_cache_retention
  side-effect field is stripped proxy-side before forwarding upstream.
- hermes: write a bili-session-identity provider plugin into the
  ~/.hermes-bili overlay — it re-registers the bundled custom profile
  (plus custom:<name> for every proxied provider) with the real hermes
  session id as prompt_cache_key. Skipped when ~/.hermes already has a
  plugins/ dir (never write through the overlay symlink).
- fix: prepareHermesHome now strips YAML quotes before matching api
  values — quoted endpoints previously failed to rewrite silently.
- prompt_cache_retention stripped on the openai and responses paths
  (OpenAI-host-only directive; third-party upstreams may reject it).
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