Skip to content

feat(session): unify session identity and identify sub-agents from real client signals - #4644

Open
sususu98 wants to merge 2 commits into
router-for-me:devfrom
sususu98:feat/session-affinity-followups
Open

feat(session): unify session identity and identify sub-agents from real client signals#4644
sususu98 wants to merge 2 commits into
router-for-me:devfrom
sususu98:feat/session-affinity-followups

Conversation

@sususu98

Copy link
Copy Markdown
Collaborator

Two commits. The first unifies session extraction and affinity routing; the second
identifies sub-agents from signals captured off real clients.

Why

#4554 landed native client session signals. Two follow-up problems remained.

Routing still treated every recognized session identifier as a separate session,
so a request that arrived through a different signal than the one that created the
binding started a second binding on another credential. And sub-agent traffic was
not identified at all, because the headers that carry it were never read.

To find out what clients actually send, rather than infer it, requests from
Claude Code 2.1.220, Codex 0.145.0, OpenCode 1.18.4 and pi 0.82.1
were captured against a local gateway that replies with each client's own
sub-agent tool, so a real child agent request is observed. Three different
conventions turned up.

Commit 1 — unified extraction and alias affinity

Every recognized explicit root identifier in one request is treated as an alias of
the same session, so a request seen through any of them resolves to one binding.
Thread, user, transport, derived and message-hash fallbacks stay isolated from
explicit roots. Local bindings are namespaced by caller, provider and model. Cold
and overlapping alias groups resolve atomically under striped alias/group locks,
conflicts merge symmetrically, and unavailable auths rebind. The cache is bounded
by TTL and LRU. Home receives both the legacy session ID and structured identity
metadata. Confidence stays observational and never affects routing.

Commit 2 — sub-agent identification

Claude Code repeats the root session at every depth and marks a sub-agent with
X-Claude-Code-Agent-Id, which the main agent omits entirely. The parent
identifier only appears from depth 2, because a depth-1 sub-agent's parent is the
main agent and has none of its own:

main agent    session=5fea96b2…  agent=-                  parent=-
subagent d1   session=5fea96b2…  agent=a0f476550f303bf2d  parent=-
subagent d2   session=5fea96b2…  agent=a15f3b017b93ec1d8  parent=a0f476550f303bf2d

Neither header was read, so thread identity was always empty for Claude Code.
Both now feed ThreadID/ParentThreadID.

OpenCode inverts the convention: a sub-agent gets its own X-Session-Id and
reports the root in X-Parent-Session-Id, so it was routed as an unrelated
session and lost the parent's prompt cache. The parent is now collected under the
same header: prefix as X-Session-Id, so a child joins the parent's alias group
and reuses its credential.

Codex was already correct: session-id stays on the root while thread-id
moves to the child. But x-client-request-id follows the thread, not the session,
so collecting it as an unconditional root added one throwaway alias per sub-agent
to the root alias group. It is now a last resort, still providing affinity for a
client that sends nothing else.

pi exposes no sub-agent tool at this version.

Claude Code also reports background work as X-App: cli-bg, which a strict cli
comparison dropped, costing one of the two signals header-only detection needs.

Codex reports per-turn observations in X-Codex-Turn-Metadata. RequestKind,
ThreadSource and TurnID carry them to Home, usage records and request logs so a
session timeline can separate real conversation turns from compaction, title
generation and other housekeeping, and can group the upstream requests belonging
to one client turn. They are observation only, never enter a routing key, and
client-supplied JSON is bounded and rejected on control characters.

Compatibility

New wire fields are additive and omitempty. Home reads the auth dispatch and
usage payloads with gjson, which ignores unknown fields, so a new CPA runs against
an older Home with no ordering constraint. This was verified by reading Home's
decoders, not by an end-to-end run against a live Home.

Verification

gofmt clean, builds, and the full suite passes (86 packages). Under -race the
only failing packages are internal/runtime/executor and internal/watcher,
which fail identically on an unmodified tree.

Every changed production file was reverted individually to confirm a test fails or
the build breaks, including the field-copy paths into Home and usage that would
otherwise publish empty values silently. Two cases are worth calling out:

  • an OpenCode sub-agent must pick the parent's credential — reverting the
    extraction change makes it pick a different one
  • a parent with 60 sub-agents exceeds the per-group alias cap; the parent keeps its
    binding, and an early sub-agent whose own aliases were squeezed out still
    resolves through the parent header

The one change that alters routing rather than only observation is the OpenCode
parent alias. Its semantics were verified on OpenCode only.

sususu98 added 2 commits July 28, 2026 18:02
Treat every recognized explicit root session identifier as an equivalent
alias while keeping the representative ID display-only. Keep thread, user,
transport, derived, and message-hash fallbacks isolated from explicit roots.

Namespace local bindings by caller, provider, and model. Resolve cold and
overlapping alias groups atomically with striped alias/group locks so the
same session's concurrent first requests return one auth without serializing
unrelated sessions. Merge conflicts symmetrically, rebind unavailable auths,
and retain bounded TTL/LRU behavior.

Send Home both the legacy session ID and structured identity metadata,
including aliases, source, confidence, scope, client type, and thread
relationships. Scope CPA's compatibility alias cache by caller and propagate
the exact canonical Home dispatch session into direct, count-token, and
streaming usage contexts. Confidence remains observational and never affects
routing.

Make SessionCache shutdown idempotent and stop replaced affinity selectors
during routing reconfiguration. Expand extraction, transition, caller
isolation, cache-bound, lifecycle, and concurrent first-pick coverage.
Captured live traffic from Claude Code 2.1.220, Codex 0.145.0, OpenCode 1.18.4
and pi 0.82.1 against a local capture gateway that replies with the client's own
sub-agent tool, so a real child agent request is observed rather than inferred.
Three conventions turned up, and only Codex was already handled correctly.

Claude Code repeats the root session at every depth and marks a sub-agent with
X-Claude-Code-Agent-Id, which the main agent omits entirely; the parent
identifier only appears from depth 2, because a depth-1 sub-agent's parent is the
main agent and has no identifier of its own. Neither header was read, so thread
identity was always empty for Claude Code. Both now feed ThreadID/ParentThreadID.

OpenCode inverts the convention: a sub-agent gets its own X-Session-Id and
reports the root in X-Parent-Session-Id. The parent is now collected under the
same "header:" prefix as X-Session-Id, so a child request joins the parent's
alias group and reuses its credential instead of routing as an unrelated session
and losing the parent's prompt cache.

X-Client-Request-Id becomes a last resort instead of an unconditional root
identifier. A Codex sub-agent sends its own thread identifier there while
session-id stays on the root, so collecting it alongside a real root added one
throwaway alias per sub-agent to the root alias group. It still provides affinity
for a client that sends nothing else.

Claude Code reports background work as X-App: cli-bg, which a strict "cli"
comparison dropped, costing one of the two signals header-only detection needs.

Codex also reports per-turn observations in X-Codex-Turn-Metadata. RequestKind,
ThreadSource and TurnID carry them to Home, usage records and request logs so a
session timeline can separate real conversation turns from compaction, title
generation and other housekeeping, and can group the upstream requests belonging
to one client turn. They are observation only and never enter a routing key;
client-supplied JSON is bounded and rejected on control characters.

Every changed file is covered by a test that fails when the change is reverted,
including the field-copy paths into Home and usage that would otherwise publish
empty values silently, and the interaction between the OpenCode parent alias and
the per-group alias cap.
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