From 9d6012834b953a91390c02323a4588baf83a4b39 Mon Sep 17 00:00:00 2001 From: Vadym O Date: Thu, 12 Nov 2026 00:57:50 +0800 Subject: [PATCH] fix(claude): keep session identity when the per-turn tk is oversized conversationIdFromClaudeMetadata returned undefined for any user_id over 4096 characters before dropping tk. The result also gates claudeNativeSessionId, bridge-pool affinity and the Go lane, so an oversized tk with a short session cost the request its session-stable routing. Drop tk first, then apply the bound, as before #8423. Reported by Codex on #8423. Co-Authored-By: Claude Code --- src/server/request-log-conversation.ts | 4 ++-- structure/dashboard-and-usage.md | 2 +- tests/usage/request-log-conversation.test.ts | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server/request-log-conversation.ts b/src/server/request-log-conversation.ts index e1a64f7fd7e..07727fb37fa 100644 --- a/src/server/request-log-conversation.ts +++ b/src/server/request-log-conversation.ts @@ -248,8 +248,8 @@ export function conversationIdFromClaudeMetadata( metadata: { user_id?: unknown } | null | undefined, ): string | undefined { if (!metadata || typeof metadata.user_id !== "string") return undefined; - // Too long to persist: skip parsing a blob normalizeLogConversationId rejects anyway. - if (metadata.user_id.trim().length > LOG_CONVERSATION_ID_INPUT_MAX) return undefined; + // No length guard before stripping tk: an oversized tk still leaves a valid session id, + // which also gates Claude routing identity (claude-messages.ts claudeNativeSessionId). return normalizeLogConversationId(claudeSessionUserId(metadata.user_id)); } diff --git a/structure/dashboard-and-usage.md b/structure/dashboard-and-usage.md index 8db118c606e..21244af7b5c 100644 --- a/structure/dashboard-and-usage.md +++ b/structure/dashboard-and-usage.md @@ -569,7 +569,7 @@ re-validated on read; the Logs list shows it as a text badge, the detail dialog `src/server/request-log-filter.ts` owns the `/api/logs` query filters including `protocolMode`; its single-pass query applies provider, conversation, model, account, protocol mode and status before `tail`, then reports the pre-pagination count alongside the offset/limit page. `src/server/request-log-filter.ts` accepts the full provider/model label shown by `ocx logs` and bare model IDs, including IDs containing slashes. Qualified failover matches bind provider/model from the same attempt; matching occurs before the row cap and pagination. [Protocol Paths](data-planes/protocol-paths.md) owns its derivation. -Conversation searches in the dashboard, `/api/logs` and `ocx logs filter --conversation` accept `codex://threads/` links up to 512 characters, including query/fragment metadata, trailing slashes and surrounding whitespace. The server matcher in `src/server/request-log-conversation.ts` and its browser mirror in `gui/src/log-conversation-id.ts` try both the extracted single-segment id and the original trimmed query, preserving raw-id and whole-literal-URI digest matching. The prefix is case-insensitive; the id retains its case. Malformed or longer links remain plain queries subject to the existing 4096-character digest-input bound. A pasted Claude Code `metadata.user_id` adds one more candidate, the JSON object without its per-turn `tk` (`claudeSessionUserId`), because the Messages inbound stores the session under that form; any turn's `user_id` therefore finds the whole session, and the raw candidate still matches rows logged before `tk` was stripped. A query over 4096 characters is not parsed, and `conversationIdFromClaudeMetadata` skips a `user_id` over that bound before parsing, while the inbound cache key still drops `tk` at any length. Persistence is unchanged. +Conversation searches in the dashboard, `/api/logs` and `ocx logs filter --conversation` accept `codex://threads/` links up to 512 characters, including query/fragment metadata, trailing slashes and surrounding whitespace. The server matcher in `src/server/request-log-conversation.ts` and its browser mirror in `gui/src/log-conversation-id.ts` try both the extracted single-segment id and the original trimmed query, preserving raw-id and whole-literal-URI digest matching. The prefix is case-insensitive; the id retains its case. Malformed or longer links remain plain queries subject to the existing 4096-character digest-input bound. A pasted Claude Code `metadata.user_id` adds one more candidate, the JSON object without its per-turn `tk` (`claudeSessionUserId`), because the Messages inbound stores the session under that form; any turn's `user_id` therefore finds the whole session, and the raw candidate still matches rows logged before `tk` was stripped. A query over 4096 characters is not parsed. `conversationIdFromClaudeMetadata`, like the inbound cache key, drops `tk` at any length before applying that bound, so an oversized `tk` keeps the session id that also gates Claude routing identity. Persistence is unchanged. Rows may also carry `genStartMs` and `lastOutputMs`, an observed request-relative pair recorded by `src/server/request-log-generation-window.ts` from the Responses and Anthropic SSE taps: the first output item or content block (reasoning included) to the last output delta. They are proxy observation times, not provider-internal token timing, and `src/usage/log.ts` keeps them only as a pair of nonnegative finite values with `lastOutputMs >= genStartMs`. `decodeTokPerSecondResult` in `src/server/management/shared.ts` prefers that window when the pair is valid and otherwise falls back to the post-TTFT window (`durationMs - firstOutputMs`); both keep the one-second minimum, the `ttft_missing` / `decode_window_too_short` unavailable reasons and `estimated: true`, so a short measured window is still an estimate rather than a provider decode benchmark. The end-to-end tok/s column is unchanged, attempts carry no generation window of their own, and the request-history view does not present one. Successful decode DTOs additionally expose `timingBasis: "generation-window" | "legacy-post-visible-output"`, derived at read time only. Logs labels the method “Generation window” or “After visible output” in the list and attempt table and uses “Output rate during generation (est.)” or “Output rate after first visible output (est.)” in request details. Missing or unfamiliar bases show “Timing unknown” and “Output rate (est.; timing method unknown)” in details rather than inferring a method. The detail grid shows a full-width muted caption explaining the method only when the decode rate is available. Both numerators include reported reasoning output tokens, so the two estimates are not directly comparable. Historical JSONL and end-to-end rates are unchanged. Request-history selectors longer than 130 characters persist as a prefix plus a digest of the complete diff --git a/tests/usage/request-log-conversation.test.ts b/tests/usage/request-log-conversation.test.ts index 494b134efe8..b4a77351f79 100644 --- a/tests/usage/request-log-conversation.test.ts +++ b/tests/usage/request-log-conversation.test.ts @@ -266,6 +266,8 @@ describe("conversationIdFromClaudeMetadata", () => { const huge = JSON.stringify({ session_id: "s", tk: "t", pad: "x".repeat(5000) }); expect(conversationIdFromClaudeMetadata({ user_id: huge })).toBeUndefined(); expect(JSON.parse(claudeSessionUserId(huge))).not.toHaveProperty("tk"); + // An oversized tk alone does not cost the session its identity (it also gates routing). + expect(conversationIdFromClaudeMetadata({ user_id: turn("t".repeat(5000)) })).toBe(digest32(session)); }); });