Well, I may have found The Right Way to accomplish my timed reads. Term::ReadKey has a ReadKey() function (exported by default) that accepts a timeout and returns a first keystroke or undef if the timeout occurs. So I can write a timed_read() routine around it which goes ahead and reads until $/ with <>, prepends the first character, and moves on.
I've already got a routine with IO::Select, but it appears to not be working sometimes, and I don't know why. Sometimes it times out when the input should be there. I'll implement the ReadKey() version and see if it makes a difference.
Of course, when I discovered ReadKey() gave me what I needed, I looked to see how it was implemented. The answer was that the author checks far more things about the system than I knew existed and then eval's one of many ReadKey() definitions into existence. Wow! Talk about a motivator for calling a module instead of cut-and-paste.
I'm doing all this over sockets, through inetd, just to be difficult, you know.