<feed xmlns='http://www.w3.org/2005/Atom'>
<title>pardes.git/src/9p.zig, branch release-0.24</title>
<subtitle>Pardes</subtitle>
<id>https://git.0x4200.cafe/pardes.git/atom?h=release-0.24</id>
<link rel='self' href='https://git.0x4200.cafe/pardes.git/atom?h=release-0.24'/>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/'/>
<updated>2026-10-01T03:12:17Z</updated>
<entry>
<title>A connection holds up to 128 reads, the next refused "too many reads waiting: 128", and a mount beside 40 held event reads still answers</title>
<updated>2026-10-01T03:12:17Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-30T16:19:44Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=92ef244c8a24583896feb47c8c48ec40aa04ebb9'/>
<id>urn:sha1:92ef244c8a24583896feb47c8c48ec40aa04ebb9</id>
<content type='text'>
Two limits met. pardes parked 32 reads per connection and refused the
next with EAGAIN's C string, "Resource temporarily unavailable". 9ns kept
32 tags and 31 workers, each pinned by a held read. So 31 followers
through one mount took every worker, and `cat layout` then queued for
ever: the whole mount deadlocked.

cloud9 (705be665, pushed to sr.ht and pinned here) now refuses in words
past the cap. Its 9ns window is 256, so a mount always has workers past
what pardes holds. pardes raises its cap to 128. fs.md documents the limit
beside held reads. selfmount.py holds 40 event reads through the mount and
reads layout beside them. It times out with the 9ns installed before this
change and passes with the new one.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>The 9P server offers 64 KiB frames, not 8 KiB</title>
<updated>2026-10-01T03:12:17Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-29T21:36:10Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=a15c05b3d15bc269b3aec195c5e813a0f0203d4f'/>
<id>urn:sha1:a15c05b3d15bc269b3aec195c5e813a0f0203d4f</id>
<content type='text'>
8192 was kept for round 24 because 64 KiB seemed to hang fs-test's restore detached space; that was the batch-wake overflow fixed in kmqtwqsm, which any msize could hit, and fs.py passes at 64 KiB with it. A client asking for more now gets it, so a write up to 64 KiB less 24 is one Twrite, and round 24's no-newline cutoff (write_room, the negotiated msize less 24) follows: fs.py checks a 20000-byte ctl line from a 64 KiB client is taken whole. Each of the 16 connection slots holds its buffers at the larger size.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Answer 9P on the connection's task, so a session can open its own tree</title>
<updated>2026-10-01T03:12:14Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-22T14:15:46Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=31cb659ded4cf50af5903fc107f8c868ee3c7311'/>
<id>urn:sha1:31cb659ded4cf50af5903fc107f8c868ee3c7311</id>
<content type='text'>
The editor's loop was the only thing that could answer a 9P request, which
made the editor's own syscalls through a mount of its own tree -- a Look at
/mnt/9p/pardes/&lt;me&gt;/anything under a `9ns --mntgen` view, a Save into it --
requests only the blocked loop could serve. The name-based refusal that
followed (ownMountSuffix) and the in-process routing of a mount of oneself
(Client.sameSession) were patches over that, and both are gone, with the
mailbox that shipped every request to the editor's thread.

One rule replaces them, `pardes.turn`: the core is single-threaded, the
editor's thread has the turn by default and gives it up in two kinds of gap
-- while it waits for input and while a step of it is out in a host syscall
-- and a cloud9 connection task takes it in those gaps to answer. `out`
counts the steps that are out, from any thread: while one is, the core reads
consistently but that step still holds pointers into it, so a request that
would change a pane (a write, a truncation, an rmdir) is parked in the
engine and retried when the turn is next given up with nothing out, and the
editor's own wake waits for the count to reach zero. It is never a write of
its own that a step waits on out there -- writes come from a shell
performing a save between steps -- so a parked request is never the
syscall's own, and making a pane or rendering a screen need not park:
every yield sits before its step's mutation, so the layout and the surface
are whole under it. A changing request that queued effects is answered
once the editor has performed them (`echo Save &gt; exec` returns with the
file written, as acme's `put` does), and it settles the way a step does,
because without that a /log reader waited for the user's next keystroke.

Every host syscall on a user path has to give the turn up, not fs.zig's
alone: the first end-to-end run hung in `inotify_add_watch` performing the
new pane's watch effect. PDFs and images are read whole at open, so no
draw goes out into the host. The core's allocator takes its fixed buffer
through the lock-free interface, since a connection task allocates while
the editor's thread is out in a syscall that allocates too. A Restore puts
the replacement in first and releases every task waiting on the old core.

cloud9 (pinned at eb1a104) parks an open, a truncating wstat, a clunk and a
remove on `again`, not only reads and writes, and answers a parked job
whose fid was clunked without asking the backend.

Verified: test/selfmount.py runs the editor under `9ns --mntgen` and
Looks at, reads and Saves its own tree through the mount; a unit test pins
that a change parks while the editor is out mid-step and lands when it
rests, while a read is answered in the window. 9P over the Unix socket
against a tty session, same machine, Debug builds: a read of /index 278us
-&gt; 61us, a truncating body write 1184us -&gt; 609us, exec Save 718us -&gt; 583us;
the gesture benchmark is unchanged (geometric mean 0.997 over 53 cells).

Also from the reviews: a notice chip over an image or PDF pane was painted
out by the picture drawn after the cells, so pictures give up the rows; in
the GUI a tree-sitter context band painted over the chip, so body layers
are emitted first; a message is one row of printable text, its 256-byte
cut never leaves half a glyph, and one wider than its pane keeps its tail
(the file name, the reason) rather than its head.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Use cloud9's file-server engine instead of the private copy</title>
<updated>2026-10-01T03:12:14Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-20T03:59:34Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=2ce8956c8e9843eba7407ab12593871ee451b991'/>
<id>urn:sha1:2ce8956c8e9843eba7407ab12593871ee451b991</id>
<content type='text'>
src/9p.zig shrinks to an 88-line alias: the engine and the backend contract
(Req, Reply/ReplyWith, Op, Status, Attr, E, error strings) now come from
cloud9.fs. The editor's limits become cloud9.fs.Options values; the ESP32-P4
board backend drops its own copies of the contract types. No behaviour
change; fs-bench still allocates nothing per request. cloud9 re-pinned to
the commit that carries the engine.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Flatten the 9P control tree and move it out of fs.zig</title>
<updated>2026-10-01T03:12:14Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-20T03:36:50Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=c1990b3e6e196ad41379aa432bf3ccca8a65d9f0'/>
<id>urn:sha1:c1990b3e6e196ad41379aa432bf3ccca8a65d9f0</id>
<content type='text'>
The served tree loses the self/ level: /index /ctl /new /log /screen
/listeners /pane/&lt;n&gt;/... /os, with /src only in -Dembed-sources=true builds
(default off, on for esp32p4). ctl speaks the editor's own language with two
lowercase verbs, look TEXT and exec TEXT, plus acme's addr verbs; the new/
factory directory becomes one clone file; cons is gone (exec Msg); name and
sel are files; stats report real lengths, modes and mtimes; /log streams
pane new/del/rename/save events. The tree code lives in src/ninep/
(tree, pane, ctl, addr, pty, events, screen, sources); fs.zig keeps host
access, mounts, resolution and find/grep. Same engine and transports.
README (fs-help.txt) and docs rewritten; tests updated and extended.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Add Linux Tty9p mounted terminals and forward raw TTY keys</title>
<updated>2026-09-15T20:24:42Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-15T00:26:47Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=a624a56e289e4a02a411f83741f0f2c9fc0f0b2e'/>
<id>urn:sha1:a624a56e289e4a02a411f83741f0f2c9fc0f0b2e</id>
<content type='text'>
</content>
</entry>
<entry>
<title>9p: use cloud9 protocol sessions and transports</title>
<updated>2026-09-15T20:24:42Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-14T16:55:41Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=95681ff7017b8a9e4c8f9fa6a7371d1233432f2f'/>
<id>urn:sha1:95681ff7017b8a9e4c8f9fa6a7371d1233432f2f</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Refactor panes and filesystem; replace FUSE with 9P</title>
<updated>2026-09-07T16:59:12Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-06T21:11:36Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=60367d8fe23f6af98ec28e3cf6c2094dfe332df0'/>
<id>urn:sha1:60367d8fe23f6af98ec28e3cf6c2094dfe332df0</id>
<content type='text'>
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples.

Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
</content>
</entry>
<entry>
<title>9p: the client half, and a board that serves its own tree over the UART</title>
<updated>2026-08-28T01:07:32Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-27T19:42:15Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=147ebd4a36ec7199074ba05bcfb79d4a656c0b74'/>
<id>urn:sha1:147ebd4a36ec7199074ba05bcfb79d4a656c0b74</id>
<content type='text'>
Step 5 of the 9P chain (docs/9p.typ 12.5, docs/registry.typ 9P-22, 9P-11, BOARD-1).

THE CLIENT. `Client` in src/9p.zig is the mirror of `Server` and the same shape:
sans-io, no allocator, no threads, no descriptor, caller-owned buffers, and it
builds freestanding. 152 bytes of struct against the server's 9,488, because a
client owns neither a fid table nor a park table -- the far end does.

The API is submit / push+output+wrote / take. Completion is a PULL: a callback
would fire inside push, inside the transport's read, inside the host's poll
dispatch, which is exactly where fs9_service says filesystem work must not
happen. `take()` returns the next completed operation or null, which is
`Server.next()`'s loop-until-null contract read from the other side. Tags are a
fixed 16-entry table indexed BY the tag, so an out-of-order reply -- which 9P
allows and both reference clients rely on -- costs one bounds check. The reply's
TYPE is checked against the request's op, because a tag is only as good as the
table behind it. A `Done` borrows the input buffer and is valid until the next
call; `take()` releases the previous frame on entry, so the rule is mechanical
rather than remembered, and read data and error strings are zero-copy.

And one real caller, so this is not a library with no user: the `9p` word takes
a dial and a path, walks another instance's tree, and opens the bytes in a pane
like any other `Look`.

THE BOARD. A SECOND image, not a second role: the console runtime keeps UART0
bidirectionally and is behaviourally untouched. On the new one the UART carries
9P AND NOTHING ELSE -- no ANSI, no vaxis, no allocator, no heap module. The loop
is uart.read -&gt; push / retry+next -&gt; handle -&gt; reply / output -&gt; writeSome -&gt;
wrote. `writeSome` is new and additive: `write`'s bounded spin DROPS bytes on a
stalled transmitter, which on a protocol stream truncates a reply mid-message
and desynchronises for good, where a short count cannot. BOARD-1's one divider
write raises the line to 921600.

  88,000 B text, 49,424 B bss, an 88,080-byte image -- 5.7% of the 1,536,000 B
  partition, against the console image's 809,536 B.

THE COMPTIME BRIDGE, which is the part worth reading. `board9p.caps` is the ONLY
place the GPIO tree is described; node ids, parents, names, permissions,
handlers, buffer size and the per-pin directories are all derived from it, and
`fan.dirs` makes `gpio/&lt;n&gt;/value` one table entry serving eleven pins. Modes are
derived from which handlers a file has rather than declared. A second capability
is a table entry, not new tree code.

JP1 became a real table in the new leaf `src/board_pins.zig`, with the ASCII
drawing RENDERED from it at comptime and the pin list COLLECTED from it -- the
9P image links no core and so cannot import board_memory.zig, and copying the
table was not acceptable. A golden test pins the drawing byte for byte, the
console's own shape test still passes, and the identical bytes are present in
all three artifacts.

PROVED. Two daemons: B read A's `/1/body` through the `9p` word into a pane,
byte-identical to plan9port's `9p read` of the same path. Both board images
build. No hardware was attached, so nothing about the board is claimed beyond
what builds and what the host tests cover.

zig build unit-test 585/585. fs-bench unchanged and still zero allocations on
every read row.

---

REVIEW FIXES FOLDED IN. Steps 3, 4 and 5 were verified on the happy path and
then adversarially reviewed by three agents; eight defects, six fixed here, five
of them reproduced with measurements before and after. Full writeup in
docs/registry.typ `9P-27`. In brief:

  * a remote crash of the WHOLE daemon: one `size[4]` of zero plus one byte hit
    `unreachable` in `fs9_service.fill`. Also 99.7% of a core when the stuck
    buffer made `room == 0` return without reading. Now `srv.dead` is a hangup,
    checked before the room guard.
  * the editor froze 177 s on a dial: `connect(2)` ran on a still-BLOCKING
    socket before the deadline existed, and a full accept backlog waits forever.
    Now non-blocking with the wait spent against the budget. After: 2.03 s.
  * a 64 KiB pty read is exactly `queue_cap` and wiped every unread byte AND
    dropped itself. `notePtyOutput` splits at half the cap. Deterministic.
  * four silent sockets denied `--fs9` forever; connections now expire on the
    same five-second rule the frontend transport already had.
  * EMFILE spun a core; the listener pauses and leaves the poll set, as the
    frontend listener does.
  * `max_fids = 32` made `find` over `9pfuse` fail with 57 consecutive
    `Rerror`s -- refuting this step's own acceptance clause. 256 for a host,
    `board_fids` 32 for the microcontroller.

Found clean and worth recording: `sig` reaches the foreground process group; the
two-namespace pty lookup is right over both transports; `PaneFile`'s u4 wall is
guarded; reader counts release on every abrupt-death path; `fs_origin` routing
and the reply arithmetic hold under probing.
</content>
</entry>
<entry>
<title>9p: serve the acme tree over 9P2000 on a unix socket, beside the mount</title>
<updated>2026-08-27T19:42:08Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-27T18:32:02Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=def843b2f59b867ee9b1d501f559f59fb335d4cc'/>
<id>urn:sha1:def843b2f59b867ee9b1d501f559f59fb335d4cc</id>
<content type='text'>
Step 4 of the 9P chain (docs/9p.typ 12.4, docs/registry.typ 9P-15/16/17/4/5).

src/9p.zig is a base 9P2000 codec and a SANS-IO server: it never touches a
descriptor, takes no allocator, starts no thread, and builds for
wasm32-freestanding and riscv32-freestanding. That is what lets the same code
serve a unix socket here and a UART on the board later.

  Server(comptime fs: type)   duck-typed on fs.Req/fs.Reply/fs.Reply.Attr, so it
                              never imports acmefs and acmefs never learns 9P
  init{ in, out, root }       the caller owns the buffers; msize is derived
  retry/next/reply            the three fs_service.Transport ops, by name
  push/output/wrote/hangup    bytes in, bytes out, partial writes supported

next() is a PUMP, not one-message-one-request: a 3-element Twalk is three
lookups, Topen|OTRUNC is a setattr then an open, Tversion is none at all.

Decisions that were open and are now taken, each recorded in the file:
  * qid.version is ALWAYS 0, which makes Linux set P9L_DIRECT and skip its
    cache -- the 9P equivalent of the FOPEN_DIRECT_IO fuse.zig relies on.
  * Every Rread is clamped to the client's count. An over-long one is a hard
    -EIO in Linux, not a truncation.
  * Rerror carries Linux's exact strerror text (registry 9P-4 option A), so a
    mount recovers the errno instead of ESERVERFAULT. Asserted as literals,
    because a typo there is 'Unknown error 526' on every mount.
  * `.` and `..` are resolved BY THE SERVER. Under FUSE the kernel does it
    and acmefs says so; 9P has no kernel, and forwarding `..` as a lookup
    would break every client that normalises a path.
  * Topen checks the perm bits itself. Under FUSE the kernel enforced them;
    over 9P nobody is above the server, and `errors` would have been readable.
  * Tcreate and Tremove are Rerror: `new/` creates a pane on WALK, so the
    capability exists and is not spelled Tcreate.

THE INTEGRATION BUG, which was not in the protocol: the daemon's push_fs_reply
sent every reply to the FUSE mount, whose park table has no 9P tag, so it
dropped it -- Tversion worked (no core involved) and Tattach hung forever. That
is exactly the 'no routing origin for the 9P descriptor' cell in the layering
table of docs/9p.typ. Session.fs_origin now carries the transport that asked.

Proved with plan9port against a live daemon serving BOTH transports at once:
9p ls / and /1, read index/ctl/tag, write /1/body, stat, a walk through
/1/../index, pane creation through `new/body`, and the two refusals arriving as
strings -- 'permission denied' and 'No such file or directory' -- confirmed on
the raw wire as Rerror text rather than numbers. A write over 9P reads back
through FUSE and a write through FUSE reads back over 9P.

msize 8192, 34,072 bytes per connection (Server 9,488 + in 8,192 + out 16,384,
out being two msize so that every reply is infallible), four connections.
zig build unit-test: 468 tests before, 503 after.
</content>
</entry>
</feed>
