summaryrefslogtreecommitdiff
path: root/src/ninep/pane.zig
Commit message (Collapse)AuthorAge
* A tag write over the limit is refused "no space: over 4096 bytes", so its ↵Gabriel Schneider6 days
| | | | | | | | | | | err record says tag: once The refusal's text began with "tag: ", and the log's err record puts the file's name before the reason. So the record read `err - col/1/tag: tag: no space: over 4096 bytes`. The text is now the reason alone, as the control-character refusal's is, and the record names the file once. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Save makes the directories its file goes in, always: for the pane's own name ↵Gabriel Schneider6 days
| | | | | | | | | | | | | | | | and Save path alike Whether a Save made missing directories depended on how the name had come about. Config's pane did, a name written into a missing directory did (since kkrqxtsp), and `Save path` and every other name did not, failing ENOENT instead. There is now one rule: a Save makes the parent directories of the file it writes, a pane's own or a terminal's scrollback included, and the make_dirs flag is gone. A directory it cannot make still fails the write with why, as fs.py's /nonexistent-pardes-dir case shows. fs.py now also saves into missing directories by Save path and by a written name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* pardes NEWFILE in a pane forwards even when its directory is missing, and ↵Gabriel Schneider6 days
| | | | | | | | | | | | | | | | Save makes the directories fs.md said FILE must already exist, while `pardes new.txt` in fact forwarded, making a pane named for it that Save creates. With a missing directory (`pardes notes/new.txt`) the launch gave up forwarding and started a nested editor inside the pane. Now the name is made absolute as written and forwarded the same way. A name written to a pane's name file that goes into a directory not there has its Save make the directories, as Config's pane does. A Save <path> into a missing directory still fails ENOENT. The Forwarding section now says what a new FILE does. fs.py launches one into a missing directory and saves it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A failure naming a long path keeps its reason, and name refuses a component ↵Gabriel Schneider6 days
| | | | | | | | | | | | | | | | | | over 255 bytes A Save of a 3000-byte path failed with the record "err 2 ctl: Save…", which lost the reason. The first cause was reportError, which formatted into 256 bytes, so the operation's path filled the buffer before the reason was written. The second was the waiting write's late failure, which took the first 256 bytes of the message row, never its end. Now reportError has room for the longest path. reportFailure also keeps the words fitted as an err is (fitErr: the path gives up its middle, the reason stays), and the late failure of a Save, a Dump, a shell or a ThemeFile takes those words. fs.md already said a name holds up to 255 bytes a component, but a longer one was taken and only failed later at Save. Now it is refused when written. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Nothing about a pane is logged before its new, and the boot terminal's new ↵Gabriel Schneider6 days
| | | | | | | | | | | | | | | carries the session directory, not / and a rename A pane named in the update that made it (Config's init file, a look renamed on open) logged `rename N <name>` before `new N`, so a follower met a serial it had never seen. The boot shell, started with no directory, was announced as `new 1 /` and renamed once its shell spoke. Now noteLog drops the rename of a pane not yet announced, since its new carries the name, and announces a pane first for any other record about it. A terminal whose shell has not said where it is is named by the session's directory, where it was started, and the shell saying so is no rename. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Pane, column and workspace tags take one set of write checks, and a refused ↵Gabriel Schneider6 days
| | | | | | | | `>` write leaves the tag as it was A column's or the workspace's tag took any length, so a write past 4096 bytes went in and a later Dump failed on it (bad dump tag), and a truncating open wiped any tag before the write after it could be refused. The column and workspace tags now refuse what a pane tag refuses: the 4096-byte limit (ENOSPC, tag: no space: over 4096 bytes) as well as the control characters they already did. A truncation of any of the three is held until the write after it is known to fit, and done with it; a refused write drops it; a close or read with no write after it does it then (so `: > tag` still clears). A test runs a truncating write too long, a control character and a bare truncation at each kind, and Dumps after. docs/fs.md says so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* An append to a body through 9P costs its own bytes, not five passes over the ↵Gabriel Schneider6 days
| | | | | | | | whole body: 60 KB appends go from 35.7 to 2.1 ms each Round 25 measured bulk body writes at about 8 ms a write. There is no frame wait in it: a profile of open, write, clunk in a loop found the flush of each close walking the whole body five times. dotOf, setDot and showOffset turned the cursor between offsets and rows by counting every newline from the top; setContent found the line index's changed span by comparing old and new byte for byte, and hashed the new text to see whether it was back to the saved one. The rows now come from the file's line index by binary search (checked against the counting at every offset), a splice tells setContent the span it changed, and the text is hashed only when its length is the saved text's. Measured over 9P on a Debug build: 60 KB appends 35.7 to 2.1 ms, 8 KB 5.3 to 0.8 ms, 1 KB 1.3 to 0.9 ms. What is left is two copies of the text an edit (the splice's and the undo snapshot's). The perf gate gains body-appends, 64 appends of 8 KB on the 50k-line file, and its three baselines are recorded again. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Terminal.zig is terminal.zig: a file of functions and no fields takes a ↵Gabriel Schneider6 days
| | | | | | | | namespace's lowercase name The last of the deferred renames, now that the tty and theme agents have landed: panes.terminal at its importers, the alias lines in Text.zig and File.zig and panes.zig's own uses following. dump.zig's Terminal struct, a dump record, is not this. The served sources list and docs/design.typ name the new file. test/perf.zig's references change, so the three perf baselines take its new harness id with their numbers as recorded. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A tag write past the limit says tag: no space: over 4096 bytes, which 9ns ↵Gabriel Schneider6 days
| | | | | | | | maps to ENOSPC as the docs say The tree answers ENOSPC, but through a mount 9ns maps the words, and tag: over 4096 bytes carried none it knows, so a shell saw EIO. The words now hold no space; docs/fs.md says ENOSPC again. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Writes held before the shell's clock is first seen no longer overflow its ↵Gabriel Schneider6 days
| | | | | | | | first step A 9P write served before a detached host's first pump holds its batch stamped at time 0, so nextWake said to wake at 20 ms while advance, seeing the clock for the first time, set stepped_ns days later and counted due - stepped_ns back past zero: an integer-overflow panic. It was what the 64 KiB msize trial hit in fs-test (restore detached space, the connection closing at the first write's clunk), a race any msize can lose. A batch's wake is now never before the next frame. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A tag write past the limit is refused whole, naming itGabriel Schneider6 days
| | | | | | A write to tag longer than the room left was cut and applied in part. It now changes nothing and fails ENOSPC, tag: over 4096 bytes; the docs give the limit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* An open's writes in a row to one place go in as one edit: a 10 MB body write ↵Gabriel Schneider6 days
| | | | | | | | is linear Each body or data write copied and hashed the whole buffer, so a write the mount cut in 8 KB pieces was quadratic: 10 MB took 50 s and a 1 MB insert into 10 MB 11 s, holding the editor's turn. An open's appends to body, or inserts going on at data's address, are now held and put in as one splice (one copy, one undo step, one line-starts pass) before any other request, the close, or the editor's step once the writes pause 20 ms. fs.py-driven: 2 MB 2.19 -> 0.16 s, 10 MB 50.17 -> 0.78 s, 1 MB data into 10 MB 11.49 -> 0.26 s. A body-2m case (2 MB in 256 KB writes on one open) joins the perf gate: 84023 -> 15785 us; baselines re-recorded. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A ctl or exec line with no newline runs with its write and fails it, unless ↵Gabriel Schneider6 days
| | | | | | | | it needs more A last line with no newline was held to the close, so printf bogus > ctl answered 0 and failed only in the log, Edit ,s/zzz/y/ too. A write shorter than its Twrite is the whole of what was written (acme takes each write whole): its tail runs then and a failure is the write's. It is held only when the parser says it needs more -- an open { block, an a/c/i text -- or when the write filled its Twrite and may go on (the listener tells the core the room, msize less its header). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Every catch unreachable, orelse unreachable and syscall assert outside tests ↵Gabriel Schneider6 days
| | | | | | | | is a real refusal or says why it cannot fire A sweep for round 23's crash: a run's answer (pty/run) was bufPrint'd into 48 bytes with catch unreachable, so a foreground program's long name (macOS gives up to 32 bytes) panicked; it now cuts at the room, keeping its newline, in 96 bytes. The rest were numbers into buffers sized for them, a braille codepoint, pthread calls on the queue's own mutex, and pdf_view's resolved outline entries: each now carries a one-line comment saying why it cannot fire. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A name refused for a leading blank says a blank at its startGabriel Schneider6 days
| | | | | | Both ends said a blank at its end. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A second line written to name is refused EINVAL, in one write or on the same ↵Gabriel Schneider6 days
| | | | | | | | open bash writes printf 'a\nb\n' a line at a time, so each line arrived as a whole name on the held open and renamed the pane twice. An open that has named its pane now refuses a further line, as a two-line write already was. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A buffer whose file was deleted on disk is dirty, and Del asks firstGabriel Schneider6 days
| | | | | | A file deleted under an open buffer left dirty 0 and let Del, Exit and the rest throw the only copy of its text away unasked. It now counts as dirty, as acme's does, and the refusals ask about it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* After a failed address, one that goes from it (., +, -) says there is noneGabriel Schneider6 days
| | | | | | addr kept the old address under a failed one, and an address written relative to it went from that old one. Such an address now fails with no address: the last one written to addr failed, until one that stands alone is written. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A PDF's body reads the text layer of the page it showsGabriel Schneider6 days
| | | | | | The body of a PDF pane read as empty. It now reads MuPDF's text extraction of the page shown, the text search and n/N already use, read-only (a write is refused, as round 22's body refusal made it). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Two opens writing one body are separate undo steps, however they interleaveGabriel Schneider6 days
| | | | | | The join mark a write left was the pane's, so another open's write joined the first open's step. Writing opens of body, data and xdata now get an open record, the mark names the open that set it, and another open's write starts its own step (the pane's one history: each turn the opens take is a step). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A name cut across writes on one open is one name, applied onceGabriel Schneider6 days
| | | | | | Each write to name was taken as a whole name, so a name a client wrote in two pieces renamed the pane twice, the first time to half a name. name's writes are now held per open, as exec's and ctl's lines are, until the newline or the close. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A language server question that finds nothing, cannot be answered or has no ↵Gabriel Schneider6 days
| | | | | | | | room fails, and so does a write to an image's or PDF's body Callers or Callees with nothing found, Hover or Lspwhy with no room for its pane, and a question asked of no file's pane said nothing, the 9P write succeeding; a body write to an image or PDF pane went nowhere. A write that asks a language server now waits for its answer (turn.lsp_answers), and what fails there -- nothing found, no answer, a backend's @none why, a placement with no room (ENOSPC) -- fails the write and logs its err. No diagnostics is said, not failed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Recent keeps only files on disk, and its cap never drops an open oneGabriel Schneider6 days
| | | | | | A renamed scratch never saved and a /virtual/ pane were recorded as recent files, and at 200 the oldest entry went even when that file was open. Opens, renames and closes now record a path only when it is a file on this host, and the cap drops the oldest closed entry. A name with a newline is not written to the kept list, where it would read back as two. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* The writes of one open of data, body or xdata are one undo stepGabriel Schneider6 days
| | | | | | bash line-buffers its stdout, so printf 'x\ny\nz\n' > data arrives as three writes and took three Undos. A write now leaves the join mark a truncation already left, so the next write on the file joins its step until the open is released; a truncation and any other edit start a step of their own. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Recent lists the files opened lately, closed ones too, and a jump back to a ↵Gabriel Schneider6 days
| | | | | | | | | | | | | | | | closed file reopens it A file closed by accident, its path forgotten, could not be found again: the jumplist dropped closed panes. Every file opened (a look, a Save of a scratch, a Restore, a rename) goes first in a list of 200, kept once, its dot noted when it closes, kept across sessions in $XDG_STATE_HOME/pardes/ recent. `Recent` (SPC f r) shows it in a reused +Recent, each row `path:line:col open|closed` a look reopens; /recent reads `open|closed <path>`. The jumplist keeps a closed file's entries, +Jumps marks them `(closed)`, and Back to one opens the file at its place. acme has no such thing; its dump and Load are the nearest. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A tag write refuses NUL and the other control characters; a Dump that fails ↵Gabriel Schneider6 days
| | | | | | | | | | | | says so A NUL written into a tag went into the dump, which the host then could not write, and Dump said nothing: a success that wrote nothing. Tag writes, the workspace's, a column's and a pane's, refuse control characters but tab and newline, DEL, C1 and bytes not UTF-8, EINVAL; and Dump reports any failure of its own instead of swallowing it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* /index shows names as the log does, one line of UTF-8, and a newline in a ↵Gabriel Schneider6 days
| | | | | | | | | | | | name as \n /index printed a name as it was, so a directory with a newline in it split its row in two, and bytes not UTF-8 went through; the log turned the newline into a space, silently another name. Both now show a name one way: a newline `\n`, other controls, DEL and C1 a space, bytes not UTF-8 `\xNN`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Every EINVAL a write gets says why; DEL in an exec or look line is a control ↵Gabriel Schneider6 days
| | | | | | | | | | | | character An empty name, a word written to log other than follow, a data, sel, dot or dirty write to a pane with no text, a truncate other than to zero, and a control character in a look line written through a held open were refused with no words, logged as `Invalid argument`. Each says its reason now; and DEL is refused in exec and look lines as in names. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A name write is one name, refused with its reason otherwise; a log record is ↵Gabriel Schneider6 days
| | | | | | | | | | | | | one line of UTF-8 A name took a second line, DEL, a C1 control or bytes that are not UTF-8, which then went into /index and /log as they were and split or garbled a reader's lines. `name` strips one trailing newline and refuses the rest, saying which (`bad character in file name: not UTF-8`); and the log turns DEL and C1 into spaces, as it did control characters, and escapes bytes that are not UTF-8 as `\xNN`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A rename alone does not make a pane dirtyGabriel Schneider6 days
| | | | | | | | | Any rename marked the pane dirty (saved revision one back), while the guards ask only whether its text was ever edited: /index said dirty for a pane Exit let go without asking. The name is no edit now, so /index, the guards and Restore agree; Save writes under the new name all the same. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A name written with a blank at either end is refused, not quietly trimmedGabriel Schneider6 days
| | | | | | | | | | `name` trimmed blanks off both ends, so `trailing.zig ` named another file than the one written without a word. acme refuses any blank in a name, `bad character in file name` (xfid.c:650); pardes takes one inside (it names files with spaces) and now refuses one at an end, and a control character, in acme's words. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A pane from pane/new is the session's directory's scratch, not the last ↵Gabriel Schneider6 days
| | | | | | | | | | active pane's No pane asked for it, so borrowing the directory of whichever pane last had the keyboard named it after an accident; acme's new window has acme's directory. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A chorded record written back with its two follow-ups runs once, with its ↵Gabriel Schneider6 days
| | | | | | | | | | | | | argument Writing a chorded exec's three records back as read ran the argument and its location as shell commands. The first record (flag 8) now takes the next record as its argument when it comes in the same write, else the one kept from the click (or, with none kept, waits for it); its two follow-up records, however they are written, are consumed as its, never run. Tested written back together and one per write. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A pane has a tagexec: a word run as a click in its tagGabriel Schneider6 days
| | | | | | | | | | The workspace had /tagexec and a column its exec, but a pane's own tag could be clicked only by an event record. pane/<n>/tagexec runs each line as a click in that pane's tag (never redirected to its event reader, as a header's is not) and reads back what it touched. The README, fs.md and the skill list it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A chorded exec an event reader holds is told its argument, and runs with itGabriel Schneider6 days
| | | | | | | | | | | | With event open, a 2-1 chord's exec went out as one record with flag 8 and nothing after it: the argument was freed unsaid. As acme does (exec.c:150-190), two records follow it, the argument's text and where it came from (file:#q0,#q1, empty when not known), and the pane keeps the argument, so the record written back runs with it (acme's own write-back, xfid.c:842, passes none; the reader was told, so it runs as the click would have). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* +Errors with no room anywhere logs its text rather than fail the EditGabriel Schneider6 days
| | | | | | | | | Edit's p and = put their output in +Errors, and when its column had no room the placement refusal failed the whole Edit. +Errors now tries every column with room, the last first, and with none its text is logged as msg records, a line each; the Edit succeeds. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* addr reads empty after a failed address instead of failingGabriel Schneider6 days
| | | | | | | | With no address after a failed write, a read of addr itself failed (EIO through a mount), which reads as a broken file rather than as no address. It now reads empty, as an unset limit does; data and xdata still refuse. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A limit write that fails keeps the limit it hadGabriel Schneider6 days
| | | | | | | | | A shell's > truncates limit, which lifted it, before the write; a write that then failed left no limit at all. The truncation now keeps the old limit until that open's write, and a failed write puts it back; a bare truncation still lifts it once its open is released. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Addresses snap to runes, not grapheme clustersGabriel Schneider6 days
| | | | | | | | | | sam and acme address runes; pardes snapped #n, line:col, dot and data's reads to grapheme clusters, so a lone combining mark or a CRLF's \r could not be addressed. #n, line:col, a search's match, dot (both ways) and data now land on rune boundaries (modal.runeStart and friends), and the tty's cell drawing is untouched. The README, fs.md and the skill say so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Columns are named by stable serials over 9P, as panes areGabriel Schneider6 days
| | | | | | | | | | | /col/<n> named a column by its index, which shifts when a column left of it closes: exactly what scripts trip on. A column is now /col/<serial>, its serial kept while it lives and never reused; /layout lines give serial and index (`<serial> <index> ...`), active names the serial, /index's last field is the column's serial, and the log says newcol and delcol with serials. Docs, the skill and the tree's README say so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* The tree serves the layout: /layout, /tag and /col/<n>/tagGabriel Schneider6 days
| | | | | | | | | | | | | A script could not see where the panes sit or edit the tags a person clicks in. /layout lists each column, its index, x and width in cells, current or not, empty or full, and its panes' serials, then the active column; /tag is the workspace tag and /col/<n>/tag a column's, read and written as a pane tag is (one line: a newline written in is a space, and a truncating write drops the one ending it); /index's lines end with the pane's column; the log says newcol <n> and delcol <n>. acme serves no column files, and the docs say so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A write to a closed pane's file says it is gone, and a terminal says why it ↵Gabriel Schneider6 days
| | | | | | | | | | | | | | takes no name `echo Del > pane/6/exec` just after pane 6 closed failed EACCES: the shell's lookup failed, it tried to create the file, and the engine refused every Tcreate as a permission error. pardes now takes Tcreate itself: refused as before (acme's fsyscreate refuses it too), ENOENT in a pane that has closed. Renaming a terminal was refused with only EPERM; it now says a terminal is named by its shell's directory, worded so a mount still maps it to EPERM. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A truncating write to tag drops the one newline that ends itGabriel Schneider6 days
| | | | | | | | | `echo X > tag` stored X and a trailing newline, which drew an empty tag row. After a truncation the newline ending each write is held back until more text follows or the open is released, so the one ending the whole of it is dropped and any other stays; `>>` keeps what it is given. Co-Authored-By: Claude Opus 5.5 <[email protected]>
*-. Merge: 9P round 6 + empty columns + render (acme theme, grips, Lift, Motion)Gabriel Schneider6 days
|\ \
| * | A column can be empty, as acme's can; new panes go where acme's ↵Gabriel Schneider6 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | makenewwindow puts them Newcol makes an empty column; closing a column's last pane leaves it empty with the keyboard on its tag; Delcol and Joincol alone take a column away; the session's last pane closing quits. The +New stand-in and replaceStillborn / stillborn_joiner are gone. Every new pane goes through exec.placeNew, which follows acme's makenewwindow (active column, empty column whole, blank space, halving the biggest) or, with Placement pardes, the old rules. ColumnTags is gone: column tags are always shown. A grip drag shows acme's box cursor. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | pty/status reads the pty's size as winsize set it, on a line of its ownGabriel Schneider6 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A dogfood agent wrote winsize 100 30 to pty/ctl and pty/status went on reporting the pane's grid, and the status line had no newline to end it. The size winsize sets is kept until the pane resizes and gives the pty its grid again; ctl's winsize read back and status both report it, and status ends with a newline. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A replace through data or body, truncate then write, is one undo stepGabriel Schneider6 days
| | | | | | | | | | | | | | | | | | A shell's printf x > data truncates the addressed range and then writes, and each pushed its own undo point, so one replace took two Undos. A truncation of data, xdata or body now lets the write that follows it at once join its step, as acme's replace is one. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | Writes made under mark 0 are one undo step, and no history is lostGabriel Schneider6 days
|/ / | | | | | | | | | | mark 0 skipped the undo point of each write, and mark 1 then pushed the text as it had become, so the text from before the batch was in no step: with no history before it, no Undo brought it back. Turning mark off now keeps the text as it is, and the writes after it are one step back to it, as acme's nomark joins them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | A shell's > on addr no longer resets it, so echo /re/ > addr searches onGabriel Schneider6 days
| | | | | | | | | | | | Truncating addr reset it to #0, so a shell's find-and-replace loop matched the first hit for ever, .+#1 read 1 1 each time, and foo x foo y foo became foobarbarbar... acme resets addr on the first open (xfid.c:105-108), for clients that hold the fid; a shell cannot. Here neither an open nor a truncation resets it: the address written is evaluated from the last one, and 0 or , starts over. Documented as a departure from acme. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | A malformed flag write reads as EINVAL through a mount againGabriel Schneider6 days
|/ | | | | | The refusal `takes 1 or 0` matched none of 9ns's words, so a mount turned it into EIO. It now starts `bad flag:`, which 9ns maps to EINVAL, as it did the bare errno before. Co-Authored-By: Claude Opus 5.5 <[email protected]>