Skip to content

Commit 43fe66b

Browse files
titoclaude
andauthored
fix: handle new string-encoded JSON metadata format for session ID extraction (#14)
Claude Code now sends metadata.user_id as a JSON string containing a JSON object (e.g. "{\"session_id\":\"UUID\"}") instead of the legacy "user_HASH_session_UUID" format. This broke session ID extraction and the LIKE-based session reload query, preventing conversation assembly. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d23c187 commit 43fe66b

File tree

3 files changed

+27
-5
lines changed

3 files changed

+27
-5
lines changed

internal/greyproxy/conversation_assembler.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
// that requires reprocessing existing conversations (e.g. new fields, linking).
2020
// When the stored version differs from this constant, the settings page
2121
// offers a "Rebuild conversations" action.
22-
const AssemblerVersion = 3
22+
const AssemblerVersion = 4
2323

2424
// ConversationAssembler subscribes to EventTransactionNew and reassembles
2525
// LLM conversations from HTTP transactions using registered dissectors.
@@ -327,8 +327,11 @@ func (a *ConversationAssembler) loadTransactionsForSessions(sessionIDs map[strin
327327
var likeClauses []string
328328
var args []any
329329
for sid := range sessionIDs {
330-
likeClauses = append(likeClauses, `CAST(request_body AS TEXT) LIKE ? ESCAPE '\'`)
330+
// Match both legacy format (session_UUID) and new JSON format (session_id with UUID value)
331+
clause := `(CAST(request_body AS TEXT) LIKE ? ESCAPE '\' OR CAST(request_body AS TEXT) LIKE ? ESCAPE '\')`
332+
likeClauses = append(likeClauses, clause)
331333
args = append(args, "%session_"+escapeLikePattern(sid)+"%")
334+
args = append(args, "%session_id%"+escapeLikePattern(sid)+"%")
332335
}
333336

334337
query := fmt.Sprintf(`

internal/greyproxy/dissector/anthropic.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737

3838
var sessionIDPattern = regexp.MustCompile(`session_([a-f0-9-]{36})`)
3939
var sessionIDJSONPattern = regexp.MustCompile(`"session_id"\s*:\s*"([a-f0-9-]{36})"`)
40+
var sessionIDEscapedJSONPattern = regexp.MustCompile(`\\?"session_id\\?"\s*:\\?\s*\\?"([a-f0-9-]{36})\\?"`)
4041

4142

4243
// AnthropicDissector parses Anthropic Messages API transactions.
@@ -205,15 +206,24 @@ func extractSessionIDFromUserID(raw json.RawMessage) string {
205206
return ""
206207
}
207208

208-
// Try as string first (legacy format)
209+
// Try as string first (legacy format or JSON-encoded object)
209210
var s string
210211
if json.Unmarshal(raw, &s) == nil && s != "" {
212+
// Legacy format: "user_HASH_account_UUID_session_UUID"
211213
if m := sessionIDPattern.FindStringSubmatch(s); len(m) > 1 {
212214
return m[1]
213215
}
216+
// New format: user_id is a JSON string containing a JSON object
217+
// e.g. "{\"session_id\":\"UUID\",\"device_id\":\"...\"}"
218+
var inner struct {
219+
SessionID string `json:"session_id"`
220+
}
221+
if json.Unmarshal([]byte(s), &inner) == nil && inner.SessionID != "" {
222+
return inner.SessionID
223+
}
214224
}
215225

216-
// Try as object with session_id field
226+
// Try as object with session_id field (direct JSON object, not string-encoded)
217227
var obj struct {
218228
SessionID string `json:"session_id"`
219229
}
@@ -228,10 +238,14 @@ func extractSessionIDFromRaw(body string) string {
228238
if m := sessionIDPattern.FindStringSubmatch(body); len(m) > 1 {
229239
return m[1]
230240
}
231-
// Also try "session_id":"UUID" pattern (new metadata format in raw JSON)
241+
// Try "session_id":"UUID" pattern (new metadata format in raw JSON)
232242
if m := sessionIDJSONPattern.FindStringSubmatch(body); len(m) > 1 {
233243
return m[1]
234244
}
245+
// Try escaped variant: \"session_id\":\"UUID\" (string-encoded JSON in body)
246+
if m := sessionIDEscapedJSONPattern.FindStringSubmatch(body); len(m) > 1 {
247+
return m[1]
248+
}
235249
return ""
236250
}
237251

internal/greyproxy/dissector/anthropic_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ func TestExtractSessionIDFromUserID(t *testing.T) {
183183
`{"device_id":"d8c2852a","account_uuid":"5a3241c6-4cbe-47e2-a7d3-981f6bf69be8","session_id":"9d4a2584-4176-4653-be44-7d5f270feb21"}`,
184184
"9d4a2584-4176-4653-be44-7d5f270feb21",
185185
},
186+
{
187+
"string-encoded json object",
188+
`"{\"device_id\":\"d8c2852a\",\"account_uuid\":\"5a3241c6-4cbe-47e2-a7d3-981f6bf69be8\",\"session_id\":\"9d4a2584-4176-4653-be44-7d5f270feb21\"}"`,
189+
"9d4a2584-4176-4653-be44-7d5f270feb21",
190+
},
186191
{
187192
"empty",
188193
`""`,

0 commit comments

Comments
 (0)