7fb5d2bc39
Port from openclaw/openclaw#112325: multibyte UTF-8 characters split across a 4096-byte pipe or PTY read boundary were decoded statelessly per chunk with errors='replace', corrupting both halves into U+FFFD mojibake in background process output (poll/log/wait/completion notifications). The foreground path already used an incremental decoder (tools/environments/base.py::_wait_for_process); this applies the same treatment to the background reader loops: - _reader_loop (select and blocking paths): one codecs.getincrementaldecoder('utf-8') per reader holds partial sequences across chunks; the finally block flushes a truncated tail as a single U+FFFD instead of dropping it. - _pty_reader_loop: same treatment for ptyprocess byte chunks (pywinpty str chunks pass through unchanged). Genuinely invalid bytes keep errors='replace' behavior.