Repository navigation
emrg: a session id the filesystem cannot name reaches mkdir and fails as an OS error #2598
Description
A session id the filesystem cannot name is accepted by the predicate that guards it, and the failure that follows names the operating system instead of the name.
What happens
Session.create_with_id builds <agent>/.emrg/sessions/<id> and mkdirs it. On Windows a directory cannot be named after a reserved device, so the mkdir throws:
is_safe_session_id('con') -> True
Session.create_with_id('con', <agent>)
NotADirectoryError: [WinError 267] The directory name is invalid:
<agent>\.emrg\sessions\con
CON, con.txt and com1 fail the same way; nul gets one step further and fails on the memory subdirectory (FileNotFoundError [WinError 3]). Measured on Windows Server 2022, master 6ddb7cc, with emrg.__file__ printed by the probe.
A second shape fails without raising: Windows strips trailing spaces and dots from a name, so an id like a or a. names a different directory than the id says. Measured: .. mkdir'd "successfully" (it resolved to the parent) and the first write into it raised FileNotFoundError.
Why the predicate is the place
An id reaches the sessions directory through two doors, and both already ask emrg.session.is_safe_session_id:
- an inbound client message, via
Daemon._refuse_an_unsafe_session_id, which covers the eleven_get_or_create_sessioncallers and thetaskdoor; - server-side creation, via
_get_or_create_sessionand the sub-session slot (Session.create_with_id, two production call sites).
So a name the platform cannot create as a directory passes both doors and fails later, deep in the filesystem. The host-visible symptom is a session that does not save or a turn that dies; the message names WinError 267, which says nothing about the name being the problem.
The predicate already refuses what no filesystem accepts (\0) and what escapes the directory (/, \, ., .., a drive), on the ground that an id reaches a path on whichever platform runs it. A reserved device name is the same kind of refusal: it names no directory on the platform that can receive it.
What must not change
My Task(spaces),中文任务(CJK) and existing ids likeemrg-taskands_260727_1103_866dcreate normally today and must keep creating.console,con-artist,com10andlpt0are not reserved and must stay accepted: the documented set isCON,PRN,AUX,NUL,COM1–COM9,LPT1–LPT9(and the same followed by an extension).COM0andLPT0are not in it.- A session directory already on disk must still load.
What the fix must satisfy
is_safe_session_idrefuses a reserved device name — case-insensitively, and with any extension, i.e. judged on the base name before the first.— and refuses an id whose last character is a space or a dot.- The refusal is platform-independent, on the predicate's existing ground: an id reaches a path on whichever platform runs it, and an id that names nothing on one of them is not portable.
Session.create_with_idraises a named error before a directory is attempted, so no filesystem error reaches the caller, and the message says which rule the name broke.- The daemon reports this reason separately from a path escape: a device name is one path component, so the existing
(a session id is one path component)sentence is false about it. - Tests in both directions: the four names above stay accepted,
con/CON/con.txt/nul/com1and a trailing space or dot are refused on both CI legs, and the mutation that deletes the device-name rule reddens the new tests.
Pointers
emrg/session.py—_NAME_SEPARATORS,is_safe_session_id,Session.create_with_idemrg/server/daemon.py—_refuse_an_unsafe_session_id(two doors, one writer)tests/test_session_delete_path.py— the existing two-direction pin of the predicate
Activity
how2how2how2-arch commentedon Oct 26, 2026
Handled by #2599
pm25coder commentedon Oct 26, 2026
Handled by #2600
pm25coder commentedon Oct 26, 2026
Correction to the line above: #2600 was withdrawn unmerged, so it is not this issue's handler.
It was opened 12 minutes before #2599 and carried a narrower version of the same fix (the reserved-device-name and trailing space/dot rule in the session-id predicate). #2599 carries the same rule through a single
session_id_refusal_reason()producer, so it also feeds the daemon's refusal message,Session.delete's log line andcreate_with_id. One issue is finished by exactly one PR, so #2600 was closed and #2599 kept.#2599 is the PR that closes this issue — the "Handled by #2599" line is the one that holds.
For the record, the reading behind the rule, taken on master
6ddb7cc:con/CON/con.txt/com1/aux/prn/nul/lpt9all passis_safe_session_id()whilecreate_with_id()fails on them withNotADirectoryError [WinError 267](nul→FileNotFoundError [WinError 3]);My Task,中文任务andemrg-taskcreate normally;aanda.are trimmed by the filesystem, so the directory no longer equals the id.