Skip to content

feat(serving): read the session OpenCode and Kilo Code send - #1371

Merged
1a1a11a merged 3 commits into
devfrom
claude/session-id-known-agents-6fx73x
Sep 6, 2026
Merged

1a1a11a merged 3 commits into
devfrom
claude/session-id-known-agents-6fx73x

Conversation

@1a1a11a

@1a1a11a 1a1a11a commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Neither client showed a session in the console after #1368/#1369. I checked both against their own source rather than guessing, and found three separate causes.

What they actually send

Current OpenCode stamps every request to a provider it does not recognise as its own with both X-Session-Id and x-session-affinity, set to the same ses_... id, plus x-parent-session-id on a subagent run. On its own provider it sends x-opencode-session instead. Kilo Code is an OpenCode fork and builds the same block (packages/opencode/src/session/llm/request.ts in both trees).

X-Session-Id is already the canonical source, so current builds were handled all along. This adds the affinity and own-provider spellings beside it: one of the pair arriving without the other still names the session, and saying so costs two tuple entries.

x-parent-session-id is deliberately not read — it names the session that spawned this one, so reading it would merge every subagent run into its parent. There's a test pinning that.

Builds that send no header at all

The header block lives in two runners. The newer one (packages/core/src/session/runner/llm.ts) lost it and only got it back in sst/opencode#43188 on 2026-08-18. Kilo's vendored copy of that file still has no header block — it forked before the restore — so on that path Kilo sends nothing any header source can read.

An earlier revision of this PR fell back to prompt_cache_key, which both clients set to the session id. That has been removed: a cache-routing hint is not a session id. The API asks only for a value that groups similar prompts, so a client may legitimately send one that is constant, and a constant would collapse every request behind it into one apparent session. A build that sends no session header now records no session, rather than the gateway inferring one from a field that was not meant to carry it.

Removing it also settled two review findings that were both correct and both about that field: /v1/responses never reached the fallback (translation replaces the delegated body and carries over only metadata/client_metadata), and "forwarded upstream untouched" was wrong for the OpenAI surfaces (ChatCompletionRequest sets extra="ignore" and completions.py rebuilds dispatch params field by field, so the key never reached an adapter).

Neither client was even labelled — in two places

parseClientTool had no opencode rule at all, and matched Kilo only as kilo[-_ ]?code/. The user agent on their LLM requests is opencode/<version> (or opencode/<channel>/<version>/<client>) and kilo/<channel>/<version>/<client> — so every request from both fell through to the leading-token fallback.

apps/backend/serving/analytics/automation_score.py carries its own copy of that table, and had the same gap: both clients scored the unknown automation prior of 0.6 instead of the interactive 0.1, skewing their users' automation scores. Patterns and INTERACTIVE_CLIENTS are updated to match, with the real user-agent shapes in the parametrisation and the resulting 0.1 asserted.

What this does not fix

For a Kilo build on the newer runner, there is nothing on the wire to read. That needs an upstream fix or a client-side workaround, and the docs say so: upgrade, or add a chat.headers plugin that sets X-Session-ID from the session id the hook is handed.

Verification

  • make lint clean; make test → 4814 passed, 56 skipped; make docs-verify → translations match the English build.
  • Frontend vitest on parseClientTool.test.ts → 6 passed, including the two real user-agent shapes.
  • Resolver cases: the header parametrisation covers both new names; plus the pair arriving together and singly, and the parent header not being mistaken for the session.
  • The zh_CN catalog needed two hand fixes msgmerge could not make: a fuzzy marker left on a rewritten entry, and a stale translation left concatenated after the new one — which rendered that paragraph twice in Chinese and was caught by make docs-verify, not by eye.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WpETEerrhcCpsg69ucjrEt

Neither client showed a session in the console. Both were checked against
their own source rather than guessed at, and there were three separate
reasons, only one of which the resolver could fix on its own.

## What they actually send

Current OpenCode stamps every request to a provider it does not recognise as
its own with `X-Session-Id` AND `x-session-affinity`, both set to its `ses_...`
id, plus `x-parent-session-id` on a subagent run. On its own provider it sends
`x-opencode-session` instead. Kilo Code is an OpenCode fork and builds the same
block.

`X-Session-Id` was already the canonical source, so the resolver handled the
current builds all along. The affinity and own-provider spellings are added
beside it: one of the pair arriving without the other still names the session,
and it costs two tuple entries to say so. `x-parent-session-id` is deliberately
NOT read -- it names the session that spawned this one, and reading it would
merge every subagent run into its parent.

## The builds that send no header at all

The headers live in two runners. The newer one (packages/core) lost them and
only got them back in anomalyco/opencode#43188 on 2026-08-18. Kilo's vendored copy of
that file still has no header block, so it forked before the restore: on that
path Kilo sends nothing a header source can read.

Those builds do still send `prompt_cache_key`, which both clients set to the
session id, so it is read last. It gets its own source label and stays last
because it is the weakest claim here: the API asks only for a value grouping
similar prompts, so a client may legitimately send a constant, and a constant
would collapse everything behind it into one apparent session. An operator who
finds a grouping implausible can see which inference produced it.

Unlike every other body source it is NOT consumed. `metadata.session_id` is a
declaration aimed at this gateway and would 400 a native Anthropic request;
`prompt_cache_key` is a real provider parameter that raises the cache hit rate,
so taking it away to record a label we have already stored would trade an
upstream saving for nothing.

## Neither client was even labelled

`parseClientTool` had no `opencode` rule, and matched Kilo only as
`kilo[-_ ]?code/`. The user agent on their LLM requests is `opencode/<version>`
and `kilo/<channel>/<version>/<client>` -- so every request from both fell
through to the leading-token fallback. Both are matched now, Kilo first, since
it is a fork and some of its other traffic still identifies as opencode.

Tests: the header parametrisation covers the two new names; new cases cover the
pair arriving together and singly, the parent header not being mistaken for the
session, the cache key losing to every other source, and the cache key
surviving `consume_session_fields`. `make lint`, `make test` (4819 passed) and
`make docs-verify` all pass; the zh_CN catalog is updated by hand, including a
bad fuzzy match msgmerge carried onto `prompt_cache_key`.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WpETEerrhcCpsg69ucjrEt
Copilot AI lite review requested due to automatic review settings September 6, 2026 11:45
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-06T12:00:30.188016Z e77e88f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The behavior changes are narrowly scoped, well-tested, and preserve upstream request semantics (notably keeping prompt_cache_key forwarded) while improving session attribution.

Pull request overview

Extends the gateway’s session resolution and admin-console client labeling to correctly recognize OpenCode and its Kilo Code fork, including header variants they emit and a prompt_cache_key fallback for header-less builds.

Changes:

  • Backend: accept x-session-affinity / x-opencode-session as additional session header sources, and fall back to prompt_cache_key only when no stronger session declaration exists.
  • Tests/docs: add unit coverage for OpenCode/Kilo session source precedence (including explicitly not reading x-parent-session-id) and document the new sources (EN + zh_CN).
  • Frontend: teach parseClientTool to recognize real-world OpenCode/Kilo User-Agent shapes (opencode/... and kilo/...).
File summaries
File Description
apps/backend/serving/utils/session_identity.py Adds OpenCode/Kilo header sources and prompt_cache_key fallback, keeping consumption rules intact.
tests/unit/serving/test_session_identity.py Adds coverage for the new header/fallback behaviors and precedence guarantees.
docs/developer/database.md Documents the additional session-id sources and prompt_cache_key’s “read but not consumed” behavior.
docs/developer/locale/zh_CN/LC_MESSAGES/database.po Updates zh_CN translations to match the new database/session documentation.
apps/frontend/src/components/features/admin/RequestsTab.tsx Updates UA parsing so OpenCode/Kilo requests are labeled correctly in Recent Requests.
apps/frontend/src/components/features/admin/__tests__/parseClientTool.test.ts Adds tests for the new OpenCode/Kilo UA parsing cases.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f97c08221

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The parametrisation comment credited Codex CLI for the whole list, which
stopped being true when the OpenCode/Kilo names joined it. Comment only.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WpETEerrhcCpsg69ucjrEt
Per review: a cache-routing hint is not a session id. The API asks only for a
value that groups similar prompts, so a client is free to send one that is
per-prompt or constant, and a constant would have collapsed every request
behind it into one apparent session. The resolver now reads only what a client
states IS its session.

That also settles two findings on the previous commit, both of which were
correct and both of which were about this field:

  * `/v1/responses` never reached the fallback. Translation replaces the body
    the delegated handler sees and carries over only `metadata` and
    `client_metadata`, so a top-level key was dropped before the resolver ran.
  * "forwarded upstream untouched" was wrong for the OpenAI surfaces.
    `ChatCompletionRequest` sets `extra="ignore"` and completions.py rebuilds
    dispatch parameters field by field from the validated model, so the key
    never reached an adapter there. Making that claim true would have meant
    modelling a new field and sending it to every provider -- a behaviour
    change well outside this PR.

The docs no longer promise a fallback. A build that sends no session header
records no session, rather than the gateway inferring one from a field that
was not meant to carry it.

## The backend classifier mirrors the frontend one

Third finding, unrelated to the above and also correct. `automation_score.py`
carries its own copy of the user-agent table, and `kilo/...` and `opencode/...`
matched nothing there either -- so both clients scored the unknown prior of 0.6
instead of the interactive 0.1, skewing their users' automation scores while
the console labelled them correctly. Patterns and INTERACTIVE_CLIENTS updated
to match, with the real user-agent shapes in the parametrisation and the
resulting 0.1 asserted.

`make lint`, `make test` (4814 passed) and `make docs-verify` pass. The zh_CN
catalog needed two hand fixes msgmerge could not make: a fuzzy marker left on
an entry whose translation was rewritten, and a stale translation the reword
had left concatenated after the new one, which rendered the paragraph twice.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WpETEerrhcCpsg69ucjrEt
@1a1a11a
1a1a11a merged commit a6e6469 into dev Sep 6, 2026
21 checks passed
@1a1a11a
1a1a11a deleted the claude/session-id-known-agents-6fx73x branch September 6, 2026 12:27
1a1a11a added a commit that referenced this pull request Sep 6, 2026
Follow-up to #1371, correcting guidance that was wrong there.

The console still showed no session for OpenCode and Kilo after #1371 reached
staging. Claude Code and Codex rows do carry sessions, so the gateway side
works end to end and the gap is entirely client-side.

#1371 said to add a chat.headers plugin on the runner that sends no headers.
That is useless in both directions. The hook exists only on the older
packages/opencode/src/session/llm/request.ts path, which already builds the
header block itself; the newer core runner
(packages/core/src/session/runner/llm.ts, registered as SessionRunnerLLM.node
in location-services.ts) has neither the header block nor any plugin hook.
Where the hook exists nothing needs fixing, and where the headers are missing
there is nothing to hook.

Both paths were checked against the sst/opencode and Kilo-Org/kilocode trees
rather than inferred. The text now names them and the registration site so a
reader can confirm it quickly, and gives the two real remedies: upgrade
OpenCode past anomalyco/opencode#43188, and port that same change into the Kilo
fork, which branched before the restore.

Docs only. The zh_CN catalog carries both rewritten paragraphs.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WpETEerrhcCpsg69ucjrEt
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.

3 participants