Summary
Home already supports optional session-sticky credential routing (routing.session-affinity), but the current implementation is incomplete for real multi-client production use:
- Session ID extraction misses several native client headers used by Claude Code, Codex, OpenCode, pi, and gateway-injected clients.
- Sticky bindings are process-local (
SessionCache), so multi-Home deployments do not share binding state.
- Usage records / Management API do not persist a first-class client session identity, so operators cannot filter one conversation across many requests.
This issue proposes a Home-owned session identity model that serves both:
- credential sticky binding
- request/usage observability by conversation/session
Motivation
When debugging production issues, operators usually have a client session id from Claude Code / Codex / pi logs, not only a single request_id.
Today Home can answer “what happened to request X?”, but cannot reliably answer:
- which credential did conversation/session S stick to?
- show all turns of session S in order
- did failures start after compact / model switch / subagent fork?
- which request logs belong to the same client conversation?
Current gaps
Extraction (internal/cliproxy/auth/selector.go)
Current priority roughly:
metadata.user_id (Claude formats)
→ X-Session-ID
→ Session_id
→ X-Client-Request-Id
→ bare metadata.user_id
→ conversation_id
→ message content hash
Missing / incomplete high-value sources from measured client behavior:
x-gateway-session-id
x-claude-code-session-id
session-id / thread-id # current Codex form (hyphen)
x-opencode-session / x-session-id / x-session-affinity
prompt_cache_key (carefully)
body.conversation / previous_response_id chain mapping
Also:
X-Client-Request-Id is request/transport scoped and should not be treated as a root sticky session by default
- low-confidence fingerprints (message hash / IP / UA) must not silently merge conversations
Binding store
- in-memory only
- not multi-Home safe
- routing key is not clearly namespaced by authenticated API key / tenant
Observability
usage has request_id, provider/model/auth/node metadata, but no durable:
session_id
thread_id
session_source
session_confidence
client_type
So Management filters and HomeUI cannot build a conversation timeline from DB-backed usage data.
Proposed design
Design doc in this repo:
docs/design/session-binding-and-observability.md
1) Extract with source + confidence
ExtractedSession {
client_session_id
thread_id?
parent_thread_id?
client_type
source
confidence # high | medium | low
scope # session | thread | user | transport
client_provided
}
Recommended priority (simplified):
x-gateway-session-id
→ native client headers (Claude Code / Codex / OpenCode / Hermes / ...)
→ Claude metadata.user_id session fields when multi-signal confirmed
→ protocol continuity maps (conversation / previous_response_id)
→ no sticky when only low-confidence signals exist
Do not use full messages hash, IP, or User-Agent as high-confidence session keys.
2) Bind with namespaced routing key
routing_key = HMAC(
secret,
api_key_or_tenant + client_type + client_session_id + provider + model
)
Rules:
- sticky is preference + TTL, not infinite hard lock
- first writer wins (
SETNX / conditional insert)
- refresh TTL on use
- rebind when bound auth is unhealthy / model-incompatible
- invalidate bindings by auth id on cooldown/disable
Prefer shared DB-backed bindings for multi-Home clusters.
3) Persist session fields on usage
Add indexed columns (MVP):
| column |
purpose |
session_id |
operator-facing client session id |
thread_id |
optional thread/subagent id |
session_source |
where extracted from |
session_confidence |
high/medium/low |
client_type |
claude-code / codex / opencode / pi / ... |
Accept these fields from CPA usage payloads with backward-compatible aliases.
4) Management API
Extend existing usage observability filters:
GET /usage/records?session_id=...&thread_id=...&client_type=...
Add session-centric read APIs:
GET /sessions
GET /sessions/:sessionId
GET /sessions/:sessionId/requests
GET /sessions/:sessionId/bindings
POST /sessions/:sessionId/bindings/expire # optional operator action
/sessions/:id/requests should return ordered usage records so UI can render a conversation timeline and jump into existing request-log download paths.
Non-goals (this issue)
- building a full product chat transcript store
- inventing fake high-confidence sessions when clients send nothing
- treating session id as auth
- replacing request-event work; this complements request_id based diagnostics
Suggested phases
- Extractor + confidence policy + tests
- Cluster-safe sticky binding store
- Usage schema + ingest +
/usage/records filters
/sessions* Management APIs + docs
- Coordinate with HomeUI for session filter / session workbench
CPA dependency
Edge reporting should eventually include extracted session fields in usage payloads. Home parsers should tolerate missing fields so schema/API can land first.
If CPA extractor/payload work is tracked separately, link it here.
Acceptance criteria
References
- Design:
docs/design/session-binding-and-observability.md
- Current selector:
internal/cliproxy/auth/selector.go
- Current usage model:
internal/cluster/usage.go
- Current observability query:
internal/cluster/usage_observability.go
- Client header research / probe captures (local): agent session id gateway research
Summary
Home already supports optional session-sticky credential routing (
routing.session-affinity), but the current implementation is incomplete for real multi-client production use:SessionCache), so multi-Home deployments do not share binding state.This issue proposes a Home-owned session identity model that serves both:
Motivation
When debugging production issues, operators usually have a client session id from Claude Code / Codex / pi logs, not only a single
request_id.Today Home can answer “what happened to request X?”, but cannot reliably answer:
Current gaps
Extraction (
internal/cliproxy/auth/selector.go)Current priority roughly:
Missing / incomplete high-value sources from measured client behavior:
Also:
X-Client-Request-Idis request/transport scoped and should not be treated as a root sticky session by defaultBinding store
Observability
usagehasrequest_id, provider/model/auth/node metadata, but no durable:session_idthread_idsession_sourcesession_confidenceclient_typeSo Management filters and HomeUI cannot build a conversation timeline from DB-backed usage data.
Proposed design
Design doc in this repo:
docs/design/session-binding-and-observability.md1) Extract with source + confidence
Recommended priority (simplified):
Do not use full
messageshash, IP, or User-Agent as high-confidence session keys.2) Bind with namespaced routing key
Rules:
SETNX/ conditional insert)Prefer shared DB-backed bindings for multi-Home clusters.
3) Persist session fields on usage
Add indexed columns (MVP):
session_idthread_idsession_sourcesession_confidenceclient_typeAccept these fields from CPA usage payloads with backward-compatible aliases.
4) Management API
Extend existing usage observability filters:
Add session-centric read APIs:
/sessions/:id/requestsshould return ordered usage records so UI can render a conversation timeline and jump into existing request-log download paths.Non-goals (this issue)
Suggested phases
/usage/recordsfilters/sessions*Management APIs + docsCPA dependency
Edge reporting should eventually include extracted session fields in usage payloads. Home parsers should tolerate missing fields so schema/API can land first.
If CPA extractor/payload work is tracked separately, link it here.
Acceptance criteria
x-claude-code-session-idand/or confirmed metadata session fieldssession-id/thread-id; underscore forms remain compatibility onlysession_id(+ source/confidence/client_type)session_iddocs/management/api.md(+ CN if maintained)References
docs/design/session-binding-and-observability.mdinternal/cliproxy/auth/selector.gointernal/cluster/usage.gointernal/cluster/usage_observability.go