Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
279918da1This file is pinned to the selected release. Download the original to open it in your design tool.
/**
* Give this process (and therefore every child it spawns) a UTF-8 locale on macOS and Linux.
*
* Linux tools sanitize bytes they cannot represent in the current locale, and a daemon usually has NO
* locale at all: `LANG` is unset under systemd, under `docker run`, and in an ssh session that forwards
* nothing, which leaves `LC_CTYPE` at POSIX. Two measured consequences on Ubuntu 24.04, both silent:
*
* - `tmux list-panes -a -F '#{pane_id}\t#{pane_pid}\t#{pane_current_path}'` returns the TAB separators
* as `_` (0x5F, verified with od -c). `parsePanes` splits on \t, so it parses ZERO panes, discovery
* has nothing to attach an engine process to, and no agent is ever created. This is exactly the
* "tmux + claude makes no agent" report: the engine process was found, the pane was not.
* - `ps -axo comm=,args=` returns `⌘ <title>` as `??? <title>`, which kills both halves of the Command
* Code marker in engineProcessMatchScore and lets the reaper evict a live pane.
*
* Why the whole process rather than per-spawn: the daemon shells out to tmux from several modules
* (discovery, the backend adapter, pane capture, input injection) and to `ps` from two, and each one
* would have to remember. Setting it once at entry covers every current and future child.
*
* Only when nothing usable is configured — a user with `LANG=en_US.UTF-8` keeps it. `C.UTF-8` is the
* portable choice (glibc ≥ 2.35, and Ubuntu's own /etc/default/locale ships exactly it); where it does
* not exist glibc falls back to C, i.e. no worse than doing nothing.
*/
export function ensureUtf8Locale(env: NodeJS.ProcessEnv = process.env): void {
if (process.platform !== 'linux' && process.platform !== 'darwin') return
const configured = env.LC_ALL || env.LC_CTYPE || env.LANG
if (configured && /utf-?8/i.test(configured)) return
env.LC_ALL = 'C.UTF-8'
}
/**
* The environment for every `ps` call whose output is parsed.
*
* `parseProcessRow` anchors on the `lstart` column because it is the one field with a fixed shape —
* but that shape is `LC_TIME`'s, not `ps`'s, and only the C and English-US locales produce the
* `DOW MON DD HH:MM:SS YYYY` the parser expects. Measured on macOS 15.5, same machine, one `ps -axo
* pid=,ppid=,comm=,lstart=,args=` per locale, counting rows `parseProcessRow` accepts:
*
* C, en_US 592/592 Tue Sep 15 23:54:57 2026
* en_GB, en_AU 0/594 Tue 15 Sep 23:54:57 2026 day and month swapped
* de_DE, nl_NL 0/592 Di. 15 Sep. 23:54:57 2026 ...and abbreviations carry dots
* fr_FR, es_ES 0/592 mar. 15 sept. 23:54:57 2026
* ja_JP, zh_CN 0/596 火 9/15 23:54:57 2026 numeric month, no day name
* ru_RU 0/593 вторник, 15 сентября 2026 г. 23:54:57 year before the time
*
* Zero rows is not a degraded table, it is no table at all, and `processRows` returns `[]` rather than
* `null` for it — "we looked and nothing is there" — so every caller believes the machine is empty.
* `resolvePaneEngineProcess` then finds no engine under any pane and `watchCreatedPane` spends its full
* ten minutes before reporting `START_TIMEOUT`: "<engine> did not expose an engine process within 10
* minutes", on a pane where the engine is running and answering hooks. Reported against 1.1.32 / CLI
* 0.2.35 on a Mac with LANG=en_US.UTF-8 and LC_TIME=en_AU.UTF-8 (macOS keeps language and region
* separate, so this split is a two-click setting, not an exotic one). 13 agents in one session.
*
* `ensureUtf8Locale` does not cover it: it returns early whenever a UTF-8 locale is already configured,
* which on a desktop macOS is always, and it only ever sets `LC_CTYPE`'s category anyway.
*
* Widening the parser instead was rejected — `вторник, 15 сентября 2026 г. 23:54:57` and `火 9/15` are
* not the same grammar with different words, and a regex loose enough for both stops pinning the field
* boundaries that `comm`-with-spaces needs it to pin.
*
* Both categories have to be set explicitly, and `LC_ALL` cleared, because `LC_ALL` outranks both:
* - `LC_TIME=C` gives the parser the shape it documents.
* - `LC_CTYPE` stays UTF-8 so Linux procps keeps returning raw bytes rather than `?` for every
* character it cannot print (see `ensureUtf8Locale` and `repairMangledRows`).
*/
export function psEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const configured = env.LC_ALL || env.LC_CTYPE || env.LANG
const ctype = configured && /utf-?8/i.test(configured) ? configured : 'C.UTF-8'
return { ...env, LC_ALL: '', LC_CTYPE: ctype, LC_TIME: 'C' }
}