@@ -2,25 +2,66 @@ import type { SessionEntry } from "../config/sessions.js";
22import { toAgentRequestSessionKey } from "../routing/session-key.js" ;
33
44type SessionIdMatch = [ string , SessionEntry ] ;
5+ type NormalizedSessionIdMatch = {
6+ sessionKey : string ;
7+ entry : SessionEntry ;
8+ normalizedSessionKey : string ;
9+ normalizedRequestKey : string ;
10+ isCanonicalSessionKey : boolean ;
11+ isStructural : boolean ;
12+ } ;
513
6- function compareUpdatedAtDescending ( a : SessionIdMatch , b : SessionIdMatch ) : number {
7- return ( b [ 1 ] ?. updatedAt ?? 0 ) - ( a [ 1 ] ?. updatedAt ?? 0 ) ;
14+ export type SessionIdMatchSelection =
15+ | { kind : "none" }
16+ | { kind : "ambiguous" ; sessionKeys : string [ ] }
17+ | { kind : "selected" ; sessionKey : string } ;
18+
19+ function normalizeLookupKey ( value : string ) : string {
20+ return value . trim ( ) . toLowerCase ( ) ;
21+ }
22+
23+ function compareNormalizedUpdatedAtDescending (
24+ a : NormalizedSessionIdMatch ,
25+ b : NormalizedSessionIdMatch ,
26+ ) : number {
27+ return ( b . entry ?. updatedAt ?? 0 ) - ( a . entry ?. updatedAt ?? 0 ) ;
828}
929
1030function compareStoreKeys ( a : string , b : string ) : number {
1131 return a < b ? - 1 : a > b ? 1 : 0 ;
1232}
1333
14- function collapseAliasMatches ( matches : SessionIdMatch [ ] ) : SessionIdMatch [ ] {
15- const grouped = new Map < string , SessionIdMatch [ ] > ( ) ;
34+ function normalizeSessionIdMatches (
35+ matches : SessionIdMatch [ ] ,
36+ normalizedSessionId : string ,
37+ ) : NormalizedSessionIdMatch [ ] {
38+ return matches . map ( ( [ sessionKey , entry ] ) => {
39+ const normalizedSessionKey = normalizeLookupKey ( sessionKey ) ;
40+ const normalizedRequestKey = normalizeLookupKey (
41+ toAgentRequestSessionKey ( sessionKey ) ?? sessionKey ,
42+ ) ;
43+ return {
44+ sessionKey,
45+ entry,
46+ normalizedSessionKey,
47+ normalizedRequestKey,
48+ isCanonicalSessionKey : sessionKey === normalizedSessionKey ,
49+ isStructural :
50+ normalizedSessionKey . endsWith ( `:${ normalizedSessionId } ` ) ||
51+ normalizedRequestKey === normalizedSessionId ||
52+ normalizedRequestKey . endsWith ( `:${ normalizedSessionId } ` ) ,
53+ } ;
54+ } ) ;
55+ }
56+
57+ function collapseAliasMatches ( matches : NormalizedSessionIdMatch [ ] ) : NormalizedSessionIdMatch [ ] {
58+ const grouped = new Map < string , NormalizedSessionIdMatch [ ] > ( ) ;
1659 for ( const match of matches ) {
17- const requestKey = toAgentRequestSessionKey ( match [ 0 ] ) ?? match [ 0 ] ;
18- const normalizedRequestKey = requestKey . trim ( ) . toLowerCase ( ) ;
19- const bucket = grouped . get ( normalizedRequestKey ) ;
60+ const bucket = grouped . get ( match . normalizedRequestKey ) ;
2061 if ( bucket ) {
2162 bucket . push ( match ) ;
2263 } else {
23- grouped . set ( normalizedRequestKey , [ match ] ) ;
64+ grouped . set ( match . normalizedRequestKey , [ match ] ) ;
2465 }
2566 }
2667
@@ -29,66 +70,68 @@ function collapseAliasMatches(matches: SessionIdMatch[]): SessionIdMatch[] {
2970 return group [ 0 ] ;
3071 }
3172 return [ ...group ] . toSorted ( ( a , b ) => {
32- const timeDiff = compareUpdatedAtDescending ( a , b ) ;
73+ const timeDiff = compareNormalizedUpdatedAtDescending ( a , b ) ;
3374 if ( timeDiff !== 0 ) {
3475 return timeDiff ;
3576 }
36- const aNormalizedKey = a [ 0 ] . trim ( ) . toLowerCase ( ) ;
37- const bNormalizedKey = b [ 0 ] . trim ( ) . toLowerCase ( ) ;
38- const aIsCanonical = a [ 0 ] === aNormalizedKey ;
39- const bIsCanonical = b [ 0 ] === bNormalizedKey ;
40- if ( aIsCanonical !== bIsCanonical ) {
41- return aIsCanonical ? - 1 : 1 ;
77+ if ( a . isCanonicalSessionKey !== b . isCanonicalSessionKey ) {
78+ return a . isCanonicalSessionKey ? - 1 : 1 ;
4279 }
43- return compareStoreKeys ( aNormalizedKey , bNormalizedKey ) ;
80+ return compareStoreKeys ( a . normalizedSessionKey , b . normalizedSessionKey ) ;
4481 } ) [ 0 ] ;
4582 } ) ;
4683}
4784
48- export function resolvePreferredSessionKeyForSessionIdMatches (
85+ function selectFreshestUniqueMatch (
86+ matches : NormalizedSessionIdMatch [ ] ,
87+ ) : NormalizedSessionIdMatch | undefined {
88+ if ( matches . length === 1 ) {
89+ return matches [ 0 ] ;
90+ }
91+ const sortedMatches = [ ...matches ] . toSorted ( compareNormalizedUpdatedAtDescending ) ;
92+ const [ freshest , secondFreshest ] = sortedMatches ;
93+ if ( ( freshest ?. entry ?. updatedAt ?? 0 ) > ( secondFreshest ?. entry ?. updatedAt ?? 0 ) ) {
94+ return freshest ;
95+ }
96+ return undefined ;
97+ }
98+
99+ export function resolveSessionIdMatchSelection (
49100 matches : Array < [ string , SessionEntry ] > ,
50101 sessionId : string ,
51- ) : string | undefined {
102+ ) : SessionIdMatchSelection {
52103 if ( matches . length === 0 ) {
53- return undefined ;
54- }
55- if ( matches . length === 1 ) {
56- return matches [ 0 ] [ 0 ] ;
104+ return { kind : "none" } ;
57105 }
58106
59- const loweredSessionId = sessionId . trim ( ) . toLowerCase ( ) ;
60- const canonicalMatches = collapseAliasMatches ( matches ) ;
107+ const canonicalMatches = collapseAliasMatches (
108+ normalizeSessionIdMatches ( matches , normalizeLookupKey ( sessionId ) ) ,
109+ ) ;
61110 if ( canonicalMatches . length === 1 ) {
62- return canonicalMatches [ 0 ] [ 0 ] ;
63- }
64- const structuralMatches = canonicalMatches . filter ( ( [ storeKey ] ) => {
65- const requestKey = toAgentRequestSessionKey ( storeKey ) ?. toLowerCase ( ) ;
66- return (
67- storeKey . toLowerCase ( ) . endsWith ( `:${ loweredSessionId } ` ) ||
68- requestKey === loweredSessionId ||
69- requestKey ?. endsWith ( `:${ loweredSessionId } ` ) === true
70- ) ;
71- } ) ;
72- if ( structuralMatches . length === 1 ) {
73- return structuralMatches [ 0 ] [ 0 ] ;
111+ return { kind : "selected" , sessionKey : canonicalMatches [ 0 ] . sessionKey } ;
74112 }
75113
76- const structuralSorted = [ ...structuralMatches ] . toSorted ( compareUpdatedAtDescending ) ;
77- const [ freshestStructural , secondFreshestStructural ] = structuralSorted ;
114+ const structuralMatches = canonicalMatches . filter ( ( match ) => match . isStructural ) ;
115+ const selectedStructuralMatch = selectFreshestUniqueMatch ( structuralMatches ) ;
116+ if ( selectedStructuralMatch ) {
117+ return { kind : "selected" , sessionKey : selectedStructuralMatch . sessionKey } ;
118+ }
78119 if ( structuralMatches . length > 1 ) {
79- if (
80- ( freshestStructural ?. [ 1 ] ?. updatedAt ?? 0 ) > ( secondFreshestStructural ?. [ 1 ] ?. updatedAt ?? 0 )
81- ) {
82- return freshestStructural [ 0 ] ;
83- }
84- return undefined ;
120+ return { kind : "ambiguous" , sessionKeys : structuralMatches . map ( ( match ) => match . sessionKey ) } ;
85121 }
86122
87- const sortedMatches = [ ...canonicalMatches ] . toSorted ( compareUpdatedAtDescending ) ;
88- const [ freshest , secondFreshest ] = sortedMatches ;
89- if ( ( freshest ?. [ 1 ] ?. updatedAt ?? 0 ) > ( secondFreshest ?. [ 1 ] ?. updatedAt ?? 0 ) ) {
90- return freshest [ 0 ] ;
123+ const selectedCanonicalMatch = selectFreshestUniqueMatch ( canonicalMatches ) ;
124+ if ( selectedCanonicalMatch ) {
125+ return { kind : "selected" , sessionKey : selectedCanonicalMatch . sessionKey } ;
91126 }
92127
93- return undefined ;
128+ return { kind : "ambiguous" , sessionKeys : canonicalMatches . map ( ( match ) => match . sessionKey ) } ;
129+ }
130+
131+ export function resolvePreferredSessionKeyForSessionIdMatches (
132+ matches : Array < [ string , SessionEntry ] > ,
133+ sessionId : string ,
134+ ) : string | undefined {
135+ const selection = resolveSessionIdMatchSelection ( matches , sessionId ) ;
136+ return selection . kind === "selected" ? selection . sessionKey : undefined ;
94137}
0 commit comments