What is wrong
#2062 taught the three file tools to ask what they are opening, because an open is
not a read: on a FIFO a read blocks until a writer appears, on a socket until a
connection does, and a device never ends. #2073 found the same condition in the
daemon's own five path readers. Both records name the third layer and leave it:
The sibling readers emrg/memory.py MemoryFile.from_file / MemoryIndex.from_file
(same condition one layer up, except (OSError, ValueError))
MemoryFile.from_file is the memory module's single reader — fourteen call sites:
_scan, list, _rebuild_index, update, merge, promote_to_project,
get_by_filename, _resolve_filename, get_with_reason. It hands the path straight to
Path.read_text, and its callers wrap the call in except (OSError, ValueError), which
cannot bound this: a FIFO raises nothing, it simply never returns.
The read is synchronous, inside a coroutine the daemon awaits on its event loop, so the
block is the whole daemon — every session, the scheduler and the evolution loop with it.
Recovery is a restart, which belongs to the host.
Two client frames walk this reader, and the directory it walks is the agent's own
workspace (<session cwd>/.emrg/memory/):
| frame |
reader |
whose path it walks |
list_memories (the GUI memory panel) |
MemoryStore.list() |
every *.md in the memory directory |
read_memory |
MemoryStore.get_with_reason() → _scan() |
the same walk, to match an id |
Nothing exotic has to create the pipe: anything running in the workspace can (mkfifo,
a build tool, a shell child of another session), and once one exists every memory frame
against that workspace wedges the daemon.
Measurement
Checkout /Users/argszero/.emrg/evolution/emrg on master 63ee3a54; emrg.__file__
asserted inside the probe to resolve in this checkout. One fixture per arm, each call in
its own process, killed by the parent's cap — the same method #2062 and #2073 used.
<memory>/foo.md is a FIFO, <memory>/bar.md is a regular file:
ProjectMemoryStore.list() -> DID NOT RETURN within 8s
the same fixture with foo.md absent (the control):
MemoryFile.from_file(<bar.md>) -> returned in 0.00s
ProjectMemoryStore.list() -> returned in 0.00s, 1 entry
What fixes it
The predicate this repository already states once — emrg.tools.base.special_file_kind,
a whitelist of S_ISREG — reached by its path-level form, so from_file refuses a
subject it cannot read instead of opening it. A regular file is unchanged, and a symlink
is judged by its target (os.stat follows it).
Not taken
MemoryIndex.from_file / MemoryIndex.save: the same class, but the pair is not the
same shape. MEMORY.md is written as well as read, and a write to a FIFO blocks on
open for writing too, so guarding the reader alone would only move the hang. That is
a boundary of its own, and the daemon's prompt-path reader of the same file is already
covered by #2074.
- Refusing a non-regular path inside the walkers rather than at the reader: the walkers
are five of the fourteen call sites, so the reader is the only site that covers them
all.
What is wrong
#2062taught the three file tools to ask what they are opening, because an open isnot a read: on a FIFO a read blocks until a writer appears, on a socket until a
connection does, and a device never ends.
#2073found the same condition in thedaemon's own five path readers. Both records name the third layer and leave it:
MemoryFile.from_fileis the memory module's single reader — fourteen call sites:_scan,list,_rebuild_index,update,merge,promote_to_project,get_by_filename,_resolve_filename,get_with_reason. It hands the path straight toPath.read_text, and its callers wrap the call inexcept (OSError, ValueError), whichcannot bound this: a FIFO raises nothing, it simply never returns.
The read is synchronous, inside a coroutine the daemon awaits on its event loop, so the
block is the whole daemon — every session, the scheduler and the evolution loop with it.
Recovery is a restart, which belongs to the host.
Two client frames walk this reader, and the directory it walks is the agent's own
workspace (
<session cwd>/.emrg/memory/):list_memories(the GUI memory panel)MemoryStore.list()*.mdin the memory directoryread_memoryMemoryStore.get_with_reason()→_scan()Nothing exotic has to create the pipe: anything running in the workspace can (
mkfifo,a build tool, a shell child of another session), and once one exists every memory frame
against that workspace wedges the daemon.
Measurement
Checkout
/Users/argszero/.emrg/evolution/emrgon master63ee3a54;emrg.__file__asserted inside the probe to resolve in this checkout. One fixture per arm, each call in
its own process, killed by the parent's cap — the same method
#2062and#2073used.What fixes it
The predicate this repository already states once —
emrg.tools.base.special_file_kind,a whitelist of
S_ISREG— reached by its path-level form, sofrom_filerefuses asubject it cannot read instead of opening it. A regular file is unchanged, and a symlink
is judged by its target (
os.statfollows it).Not taken
MemoryIndex.from_file/MemoryIndex.save: the same class, but the pair is not thesame shape.
MEMORY.mdis written as well as read, and a write to a FIFO blocks onopenfor writing too, so guarding the reader alone would only move the hang. That isa boundary of its own, and the daemon's prompt-path reader of the same file is already
covered by
#2074.are five of the fourteen call sites, so the reader is the only site that covers them
all.