Repository navigation
Core: line pipeline (one split, one ANSI strip, Line type) - #3
Merged
MHohlios merged 7 commits intoOct 30, 2026
Conversation
added 7 commits
October 29, 2026 12:54
Server bytes become shared Line values in one streaming pass: memchr finds line ends (\n, \r\n, \n\r, lone \r), the bytes are decoded once (UTF-8 lossy or Latin-1), and only the new text goes through the ANSI parser. The pending partial line, a possible prompt, is a view of the same buffers and keeps its line number while it grows. GA/EOR finish it through prompt_end. The ANSI parser now streams, so escapes split across chunks parse; parse_line keeps its behavior. mudling-bench times the pipeline per line.
Match the prototype, which removed every \r and ended lines only at \n: a lone \r no longer ends a line, and any run of \r before \n ends exactly one. A line with no end is cut at MAX_LINE_BYTES (64 KiB, never inside a UTF-8 code point) and marked LineEnd::Cut, so a broken or hostile server can't grow memory without limit.
…witches A 64 KiB cut that falls inside a code point whose start is held from the last chunk now moves that code point to the next line, instead of replacing both halves. Switching away from UTF-8 replaces a held code point where it stands, not at the end of the line. The bench's line pipeline report keeps its internals private.
ESC ( B and the like (ECMA-48 intermediates 0x20-0x2F up to a final 0x30-0x7E) are now removed whole, even split across chunks, instead of leaving their final byte in the text. A 64 KiB line cut that falls inside an escape sequence carries it to the next line; CSI parameters past 256 bytes are skipped rather than buffered, so that carry can't grow without limit.
…lize' into wiffletree/core-line-pipeline-one-split-one-ansi-strip-line # Conflicts: # crates/mudling-bench/src/main.rs
MHohlios
merged commit Oct 30, 2026
96510f2
into
wiffletree/phase-0-rust-workspace-and-gpui-spike-virtualize
4 checks passed
MHohlios
added a commit
that referenced
this pull request
Oct 30, 2026
…gins, Luau, Avatar, map (#15) * Add Cargo workspace, core scrollback and headless benchmark mudling-core holds the ANSI parser, the ring-buffer scrollback, the MUDPad log replay and percentile stats. mudling-bench measures append and find against the PRD budgets. ffi, cli and desktop are stubs for now. * Add the line pipeline: one split, one ANSI strip per line Server bytes become shared Line values in one streaming pass: memchr finds line ends (\n, \r\n, \n\r, lone \r), the bytes are decoded once (UTF-8 lossy or Latin-1), and only the new text goes through the ANSI parser. The pending partial line, a possible prompt, is a view of the same buffers and keeps its line number while it grows. GA/EOR finish it through prompt_end. The ANSI parser now streams, so escapes split across chunks parse; parse_line keeps its behavior. mudling-bench times the pipeline per line. * Line pipeline: drop every \r, and cut lines at 64 KiB Match the prototype, which removed every \r and ended lines only at \n: a lone \r no longer ends a line, and any run of \r before \n ends exactly one. A line with no end is cut at MAX_LINE_BYTES (64 KiB, never inside a UTF-8 code point) and marked LineEnd::Cut, so a broken or hostile server can't grow memory without limit. * Add completion: last 500 commands, known words, Tab, history file Echo-off input is never recorded. The history is saved by a worker thread to its own file in the OS data folder. Adds a completion-bench binary for the 20,000-word, 1 ms p99 budget. * Add the connection layer: telnet, GMCP, MCCP2 and tokio net A sans-IO telnet parser slices server text between IAC bytes with memchr into zero-copy Bytes, negotiates ECHO, EOR, GMCP, MSSP and MCCP2, and refuses every other option, NAWS included. Subnegotiation spans chunks, MCCP2 starts mid-chunk and ends cleanly, and MSSP surfaces only NAME. The net module wraps it in a tokio task with a non-blocking handle, sends Core.Hello and Core.Supports.Set when GMCP is agreed, and offers a login hook for plugins. mudling-bench-telnet measures throughput and the cost per 4 KB chunk; examples/connect prints a server's events. * Line pipeline: keep held code points whole across cuts and encoding switches A 64 KiB cut that falls inside a code point whose start is held from the last chunk now moves that code point to the next line, instead of replacing both halves. Switching away from UTF-8 replaces a held code point where it stands, not at the end of the line. The bench's line pipeline report keeps its internals private. * Cap telnet subnegotiation payloads at 8 MiB A longer subnegotiation is dropped up to IAC SE, reported once as Event::SubnegotiationDropped, and parsing carries on, so a broken server can't grow memory without limit. * Add versioned stores, batched diffs, UniFFI bindings and iOS build mudling-core gains a store module: the §12 store set (terminal, vitals, room, map, mobs, characters, gear, settings), each with a revision that bumps only on a real change. A StoreHub coalesces a frame's changes into one batch that the shell drains on its frame tick, and wakes the shell once per batch. Memo caches derived data against input revisions. Payloads other than the terminal's are placeholders for later tickets. mudling-ffi exposes a coarse UniFFI API (open, subscribe, snapshot, drain, send a command) and builds for iOS. scripts/build-ios.sh produces the XCFramework and Swift bindings; a CI workflow runs it on macOS. mudling-bench measures a frame's batch apply and a 50 lines/s burst. * Add SQLite persistence with a writer thread One WAL database per server under the OS data folder (servers/), with a versioned schema, a writer thread that commits each batch of row upserts and deletes in one transaction, and a separate read-only connection for startup loads and lists. Map history is a header table plus rows copied from the live map in SQL, so listing reads headers only. The persist bench (mudling-bench --bin persist) measures move enqueue cost, writer throughput, history listing and a 10,000-room startup read. * Correct two completion doc comments * Line pipeline test: a generic prompt instead of a game's * Cap a coalesced terminal append at the store's capacity A shell that stops draining, such as a minimized window, no longer grows the pending terminal diff without bound; the lines dropped are ones the terminal would trim anyway. The terminal tests move next to the payload, and the bench derives its frame budget from the frame length. * Phase 0: gpui chat, IME input, AccessKit pass, measuring and report * Address connection review: buffer reuse, connect timeout, byte login answers - Reuse the read buffer, reserving only when under 4 KB remains. - Time the connect out (ConnectOptions::connect_timeout, 10 s default) and abort it on close or when every handle drops. - LoginHook::answer returns bytes in the server's encoding. - Ignore the wall-clock linear-time test; the bench measures it. - The telnet bench uses the core's GMCP framing and constants, and names its decompressed totals telnet_*. - Say why MCCP2 output blocks are 16 KB. * Announce new lines by name too, for NVDA and Orca The Windows and Linux AccessKit adapters announce a live node's name, which for a Status node is its label, so a fixed "New lines" label hid the text. The live region now carries the lines as label and value, and the AX check asserts both. * Hold the reading position from the list's real offset; announce only server lines Whether a trim keeps the view still is now decided from the list's offset at trim time, not from a flag refreshed in render, so lines that arrive between a scroll and the next frame no longer move the view. Echoed commands stay out of the live region. Scrollback::capacity and the chat's LINE_HEIGHT and FONT_SIZE constants are no longer public. * ANSI: consume escapes with intermediate bytes; carry escapes across cuts ESC ( B and the like (ECMA-48 intermediates 0x20-0x2F up to a final 0x30-0x7E) are now removed whole, even split across chunks, instead of leaving their final byte in the text. A 64 KiB line cut that falls inside an escape sequence carries it to the next line; CSI parameters past 256 bytes are skipped rather than buffered, so that carry can't grow without limit. * Persist: fix review findings - Keep listed exits, explored links and seen doorways in tables of their own, as the prototype's map does, so a link or doorway outlives the room's listing. - Store rooms per area in each map version's header. - Copy and restore map versions from one column list per table. - Keep failed batches, capped, for the session to collect with Writer::take_failures; flush still returns them. - Migrate under an immediate transaction, and retry the WAL switch, so connections opening a new file at once can't race. - Neutral host name in tests, bench labels from constants, small renames. * Add the gpui-pre verification and Linux dependency scripts; run the AX check in CI scripts/verify-gpui-pre.sh diffs the gpui-pre crates against the Zed revision they name; scripts/install-linux-deps.sh is the one list of gpui's Linux libraries, used by CI and linked from the README and docs. The macOS CI job now runs --ax-check. The report also records that headless find misses its budget on a heavily loaded machine. * Add the plugin host: API v1 manifests, merged matchers, layers A plugin is a folder with plugin.toml (id, name, version, api = 1, license, layer, servers, encoding) and one TOML file per extension point. All nine API v1 points parse into typed declarations, and every pattern compiles at load; a bad plugin is rejected with an error naming the file, and the line where TOML gives one. The host runs Text end to end: patterns emit PluginEvents with named captures and an optional Luau hook name to a PluginSink, color rules paint lines the server sent plain, and prompt hints and the pager read the pending line. Each matcher is one RegexSet behind an Aho-Corasick filter of literals every match must contain, rebuilt only when the set of enabled plugins changes. Layers run defaults, game, override; single values come from the highest layer. Each plugin's cost per line is sampled on its own. A test fails if mudling-core holds the prototype's Avatar wording. mudling-bench times pipeline plus host with 300 synthetic patterns. * Plugin host: review fixes - Only the merged text matcher gets the 16 MiB lazy DFA cache; the pending and per-plugin cost matchers keep the regex default. - read_pending caches its answer by the pending text itself, so a host reused with a new pipeline (a reconnect) never answers from the old one. - Server hosts are exact or one leading `*.domain`; any other wildcard is rejected at load. - Point, Point::ALL and Point::name come from one list. - PluginError's `file` is now `source`: a file, a plugin id or the host. - mudling-bench reports the host's heap by stage with 6 and 20 plugins. * Plugin host: less memory per pattern, chunked merged sets - Pattern wraps a regex-automata meta regex built without the one-pass DFA, which alone cost up to 440 KB for a pattern with Unicode classes. The engine is private: Pattern offers is_match and captures, and PatternError says why a pattern doesn't compile. - The per-plugin cost matchers are gone. Sampled lines time each plugin's own rules one by one; costs() is unchanged. - The merged text matcher splits its filtered patterns into sets of 64. One Aho-Corasick scan says which sets' literals a line holds, and only those run. One set of 1,000 patterns cleared its DFA cache constantly (661 µs p99 per line); in chunks it's 11 µs. - mudling-bench reports heap and host p99 for a realistic set (defaults, a 150-pattern game, an override) and for 6 and 20 uniform plugins. * Plugin host: prefiltered cost sampling; bench gates on §11's measure - Sampled lines time only each plugin's rules that the merged matcher's prefilter lets through (their required literals appear, or they have none), and extract captures only where a rule matches. - mudling-bench --check gates on the whole line (≤ 20 µs p99) and the marginal cost of one plugin (≤ 5 µs p99). Each plugin's sampled cost is reported as an ungated upper bound, beside what sampled lines cost. - The bench module's budgets and report fields are private, and PROMPT_HINTS sets the number of prompt hints each plugin gets. * Add the map model and the live GMCP automap A `map` module in mudling-core: rooms built live from Room.Info, the game's text and the player's commands, one map per server. A move updates a few rooms in memory and yields a batch of only the changed rows for the persistence writer, plus a MapChange for views with a revision that bumps only on a real change. Nothing re-encodes the world. - Layout on persistent maps (imbl), so each explanation of an unsure walk copies in O(1), with indexes for look-alikes, squares, inbound links, hubs, buildings and area counts: no move scans the world. - The prototype's tracker: the expected-move queue with the speedwalk fix, guesses scored and confirmed over later rooms, failed moves, flees, dark walks, mazes, and room identity by GMCP number or by name, exits and description. - Straightening and buildings ported; the drawing search runs on dense indices and skips choices a known contradiction already rules out, so it picks the same drawing far faster. - Derived areas and buildings cached by revision; walk-to on a VecDeque BFS; mob memory; map versions through the writer. - MapRules: the game's wording as a trait with neutral defaults. - mudling-bench `map`: move cost on a 10,000-room map and on replayed session logs, and an informational comparison with a copy of the prototype's world file. * Format the line pipeline with rustfmt * Format completion with rustfmt * Persist tests: use a plain area name * Map: a maze message marks the room in every guess * Make mudling-bench the default binary of its crate The telnet bench is a second binary, so cargo run -p mudling-bench needs to know which one to run. * Run mudling-bench by default now that the crate has two binaries * Map: review fixes (derived count test, dead returns, save_version, empty replay) * Map: room numbers overrule a move the game didn't make * No game strings test: find its own file the same way on every OS On Windows file!() uses backslashes, so trimming a forward-slash prefix left the path unmatched and the test flagged its own denylist. * Plugin host tests: build fixture paths from components On Windows the loader joins with a backslash, so the expected error prefix for a missing plugin folder is now built with Path::join and display, like the error. The fixtures folder is built the same way. * Core: line pipeline (one split, one ANSI strip, Line type) (#3) * Add the line pipeline: one split, one ANSI strip per line Server bytes become shared Line values in one streaming pass: memchr finds line ends (\n, \r\n, \n\r, lone \r), the bytes are decoded once (UTF-8 lossy or Latin-1), and only the new text goes through the ANSI parser. The pending partial line, a possible prompt, is a view of the same buffers and keeps its line number while it grows. GA/EOR finish it through prompt_end. The ANSI parser now streams, so escapes split across chunks parse; parse_line keeps its behavior. mudling-bench times the pipeline per line. * Line pipeline: drop every \r, and cut lines at 64 KiB Match the prototype, which removed every \r and ended lines only at \n: a lone \r no longer ends a line, and any run of \r before \n ends exactly one. A line with no end is cut at MAX_LINE_BYTES (64 KiB, never inside a UTF-8 code point) and marked LineEnd::Cut, so a broken or hostile server can't grow memory without limit. * Line pipeline: keep held code points whole across cuts and encoding switches A 64 KiB cut that falls inside a code point whose start is held from the last chunk now moves that code point to the next line, instead of replacing both halves. Switching away from UTF-8 replaces a held code point where it stands, not at the end of the line. The bench's line pipeline report keeps its internals private. * Line pipeline test: a generic prompt instead of a game's * ANSI: consume escapes with intermediate bytes; carry escapes across cuts ESC ( B and the like (ECMA-48 intermediates 0x20-0x2F up to a final 0x30-0x7E) are now removed whole, even split across chunks, instead of leaving their final byte in the text. A 64 KiB line cut that falls inside an escape sequence carries it to the next line; CSI parameters past 256 bytes are skipped rather than buffered, so that carry can't grow without limit. * Format the line pipeline with rustfmt --------- Co-authored-by: mhohlios <mhohlios@vyttle.com> * Core: plugin host (API v1, plugin.toml, merged matchers, layers) (#7) * Add the plugin host: API v1 manifests, merged matchers, layers A plugin is a folder with plugin.toml (id, name, version, api = 1, license, layer, servers, encoding) and one TOML file per extension point. All nine API v1 points parse into typed declarations, and every pattern compiles at load; a bad plugin is rejected with an error naming the file, and the line where TOML gives one. The host runs Text end to end: patterns emit PluginEvents with named captures and an optional Luau hook name to a PluginSink, color rules paint lines the server sent plain, and prompt hints and the pager read the pending line. Each matcher is one RegexSet behind an Aho-Corasick filter of literals every match must contain, rebuilt only when the set of enabled plugins changes. Layers run defaults, game, override; single values come from the highest layer. Each plugin's cost per line is sampled on its own. A test fails if mudling-core holds the prototype's Avatar wording. mudling-bench times pipeline plus host with 300 synthetic patterns. * Plugin host: review fixes - Only the merged text matcher gets the 16 MiB lazy DFA cache; the pending and per-plugin cost matchers keep the regex default. - read_pending caches its answer by the pending text itself, so a host reused with a new pipeline (a reconnect) never answers from the old one. - Server hosts are exact or one leading `*.domain`; any other wildcard is rejected at load. - Point, Point::ALL and Point::name come from one list. - PluginError's `file` is now `source`: a file, a plugin id or the host. - mudling-bench reports the host's heap by stage with 6 and 20 plugins. * Plugin host: less memory per pattern, chunked merged sets - Pattern wraps a regex-automata meta regex built without the one-pass DFA, which alone cost up to 440 KB for a pattern with Unicode classes. The engine is private: Pattern offers is_match and captures, and PatternError says why a pattern doesn't compile. - The per-plugin cost matchers are gone. Sampled lines time each plugin's own rules one by one; costs() is unchanged. - The merged text matcher splits its filtered patterns into sets of 64. One Aho-Corasick scan says which sets' literals a line holds, and only those run. One set of 1,000 patterns cleared its DFA cache constantly (661 µs p99 per line); in chunks it's 11 µs. - mudling-bench reports heap and host p99 for a realistic set (defaults, a 150-pattern game, an override) and for 6 and 20 uniform plugins. * Plugin host: prefiltered cost sampling; bench gates on §11's measure - Sampled lines time only each plugin's rules that the merged matcher's prefilter lets through (their required literals appear, or they have none), and extract captures only where a rule matches. - mudling-bench --check gates on the whole line (≤ 20 µs p99) and the marginal cost of one plugin (≤ 5 µs p99). Each plugin's sampled cost is reported as an ungated upper bound, beside what sampled lines cost. - The bench module's budgets and report fields are private, and PROMPT_HINTS sets the number of prompt hints each plugin gets. * No game strings test: find its own file the same way on every OS On Windows file!() uses backslashes, so trimming a forward-slash prefix left the path unmatched and the test flagged its own denylist. * Plugin host tests: build fixture paths from components On Windows the loader joins with a backslash, so the expected error prefix for a missing plugin folder is now built with Path::join and display, like the error. The fixtures folder is built the same way. --------- Co-authored-by: mhohlios <mhohlios@vyttle.com> * Core: default plugins (prompt learning, HUD fields, chaining, pagers, game detection) (#8) * Add the default plugins: prompt learning, HUD fields, chaining, pagers, game detection A server with no game plugin now gets a useful client (VYT-198, PRD §11 "Defaults that adapt"). The new `defaults` module in mudling-core: - A built-in default-layer plugin (`defaults::plugin`), loaded through `Plugin::from_files`: text passes through uncolored, and the generic pager prompts ([C]ontinue, --More--, Press RETURN) are its pager, which a game plugin's pager replaces through the host's settings. - Prompt learning, ported from the prototype's GamePrompt with its 2026-10-28 fix. Shapes are counted, the most seen first, ties shortest. Pending text is never learned while it can still grow; a prompt is learned when its line ends, at GA, or when the next GMCP message shows the server finished writing it. A full store drops the rarest shape, ties longest, so glued output can't push out the real prompt. A learned prompt in pending text is ended with prompt_end. Prompt lines are hidden by default (`Defaults::hide_prompts`). - HUD fields from the first Char.Vitals keys by common names, typed as `Vital`, with each enabled plugin's HUD vitals over them in run order. - Command chaining (separate lines without a separator, packed into the highest layer's separator within 200 bytes) and speedwalk (`3n2e`, `8n, [s], d`), sent in the Commands point's speedwalk syntax and door command when a plugin declares them. - A table-read pager that answers each pager prompt once, only between begin_table_read and end_table_read. - Game detection from MSSP NAME, then the GMCP Core.Hello name, then the host, against each game plugin's [servers]. mudling-bench replays the logs' GMCP notes through the line pipeline, the plugin host and the defaults. It reports the prompt match rate and checks that no half-prompt shape is learned over three replays split into random small reads, and it times the defaults per line. * HUD values: match GMCP package names ignoring case * Defaults review fixes: prompts start after the vitals, gmcp_arrived - Text already pending when Char.Vitals arrives, such as a pager prompt or unended output, came before it and isn't learned as the prompt. The learner counts lines and awaits the prompt only on a line that began after the vitals. - Rename Defaults::gmcp to gmcp_arrived, to match vitals_arrived. - The bench matches hud::VITALS ignoring case, fails loudly when nothing was timed, and keeps its doc's bullet list whole. * Defaults tests: build the fixture path from components --------- Co-authored-by: mhohlios <mhohlios@vyttle.com> * Core: Luau hooks (sandbox, permissions, instruction budget) (#9) * Add Luau hooks: sandbox, permissions, instruction budget Plugins list Luau files under [hooks] and declare permissions (read_state, write_store:<name>, send_commands). A pattern that names a hook hands its event to plugin::luau, which runs it on its own worker thread in a sandboxed per-plugin VM: no io, os, debug, require or loadstring, read-only globals, capped memory. An interrupt-counted budget stops a runaway hook; three overruns in a session turn the plugin off through the host with a PluginDisabled notice. Hooks emit StoreWrite and rate-limited SendCommand outputs; a plugin that sends commands runs no hooks until approved. mudling-bench times the hand-off, a trivial hook, a column-splitting hook and how fast the budget stops an endless loop. * Luau hooks: a wall-clock time limit alongside the tick budget A hook whose ticks are slow, such as big string work in a short loop, stayed under the tick budget while holding the worker for seconds. The interrupt now also checks a deadline (2 ms by default) and counts crossing it as an overrun. * Luau hooks: review fixes - Time box in thread CPU time (cpu-time crate: clock_gettime with CLOCK_THREAD_CPUTIME_ID on Unix, GetThreadTimes on Windows), so preemption, suspension or a blocked StateView read isn't charged. The wall clock only decides when the CPU time needs reading. - Loading a plugin's hooks checks that every hook its points name is defined, and rejects the plugin otherwise. - The job queue is bounded (1,024 by default); when full, a new event is dropped, never waited for, and counted per plugin in LuauHooks::dropped. - LuauHooks::start reports a failed thread spawn as StartError::Spawn. - The manifest doc example puts permissions before the tables, and a test loads it as written. - The module doc's first sentence reads properly. * Luau hooks: PluginError.file is now .source * Luau hooks: second review fixes - Compiling a hook file no longer counts against its time box; only running the chunk does. - A plugin whose hooks don't load is left out and listed in LuauHooks::failed_to_load; the others run. start fails only when the worker can't be spawned. - set_approved never waits: jobs go on an unbounded channel, and a counter bounds the events instead. The waiting call is now wait_until_idle, for tests, benches and shutdown. - turn_off_overrunning tries every plugin and returns each result. - The bench times the hand-off with the queue never full. * Luau hooks: rustfmt * Luau hooks: own error for VM setup; refund a failed hook's sends - A Luau VM that can't be set up is Problem::LuauSetup, sourced to the plugin, rather than Problem::Invalid on plugin.toml. - A hook that sends and then fails gives its rate-limit slots back, since its sends never went out. * Luau hooks: confirm a CPU-time overrun; the bench counts overruns A trivial hook overran its 2 ms box on GitHub's macOS runner. It makes one or two interrupt ticks, so the CPU-time check tripped: a stall past 2 ms of wall time made the meter read the thread's CPU clock, and on a virtual machine that clock can count time the host spent elsewhere, or advance in scheduler ticks. - Crossing the CPU-time limit now only makes a hook suspect; it overruns if it keeps running for half the limit again, so a real runaway stops at 3 ms and a hook that met one jump has returned by then. - The box starts after the event table is built, so its allocation and any GC step that runs isn't charged to the hook. - The message says which limit tripped, with the ticks or the CPU and wall time used. - The bench counts overruns of well-behaved hooks and reports the first reason; --check fails only above 1 in 10,000 calls. * Luau hooks: time-box tests that don't depend on the machine - Loading a hook file is its own function, and a unit test shows the box starts only after compiling: a file that doesn't compile never starts it. The large-file test now only checks that such a file loads. - The slow-ticks test asserts which limit stopped the hook, not how long the whole run took. - The module doc says pattern-match steps tick too. --------- Co-authored-by: mhohlios <mhohlios@vyttle.com> * Avatar game plugin (TOML + Luau, public API only) (#10) * Add the Avatar game plugin: TOML and Luau on the public API plugins/avatar holds everything the prototype hard-wired about Avatar, written only against the plugin API. The host runs only the Text point, so every line reader is a Text pattern: a table's start opens a Luau reader, its rows queue store writes, and the next prompt (or the next table) sends them. A table left at an unanswered pager sends nothing. - text.toml: the prompt hint (Avatar's two shapes and <..> prompts), the pager ("c"), score, practice, slist, aff, eq, identify, examine, wear, remove and put, failed moves, darkness, flee, area names and aggro. - readers.luau, character.luau, gear.luau: framing, fixed-column slist with wrapped rows, practice across pages, identify base vs enchant, slots and command words (never a possessive). - commands.toml (verbs, doors, no separator), gear.toml (15 slots in order), map.toml, mobs.toml (mob name from a room title), character.toml (background commands and their readers). - README.md: the event and store vocabulary. - mudling-plugin-avatar: fixture tests through Plugin::load, the host and HookRunner, and avatar-replay for local logs. - mudling-bench --plugin <dir>: a real plugin's marginal cost per line. * Avatar plugin: bench gates on marginal cost only; maze comment names the API gap * Avatar plugin tests: drop the blank line by lines, so CRLF checkouts pass * Avatar plugin: a last page with no rows still answers the pager --------- Co-authored-by: mhohlios <mhohlios@vyttle.com> * Docs: match the README and comments to the merged core The integration landed the connection layer, line pipeline, plugin host and Avatar plugin, so wording that called them future or called mudling-ffi a stub was false. * Docs: use the reviewed wording for stale comments --------- Co-authored-by: mhohlios <mhohlios@vyttle.com>
MHohlios
deleted the
wiffletree/core-line-pipeline-one-split-one-ansi-strip-line
branch
November 1, 2026 17:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Each server line is now split once and stripped of ANSI codes once, and every reader gets the same
Line(PRD §12, §5 #1).Linear: VYT-191 (Phase 1: Core, VYT-186). Stacked on #1 (Phase 0).
What's in it
line::Lineis a singleArc. It holdsraw,plain(one copy when the two are equal),styledSGR spans andlinks, which stays empty until MXP. It also records how the line ended:Newline,PromptEndorCut. It is Send + Sync and cheap to clone.LinePipeline:push(&[u8]), thenprompt_end()for GA/EOR.pending()returns the partial line, a possible prompt. Itsnumberstays the same while it grows, so half a prompt split across network reads is never taken for a whole one. That is the prototype's 2026-10-28 bug.\ris dropped and only\nends a line, as in the prototype.ansi.rsbecomes a streaming parser. Escapes split across chunks are handled, andESC ( B-style intermediates are consumed whole.parse_lineis unchanged.Numbers
On the real logs, a release build: p50 0.17 µs and p99 0.29–0.42 µs per line, against a 5 µs target for this stage.
Tests
Follow-ups
line::Line, andansi::Lineshould be retired.pending()andprompt_end()disagree while a UTF-8 lead byte is held.