You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stop shredding every compressed session by settling our own phases (#164)
* Stop shredding every compressed session by settling our own phases
Five Evennia games have had their CODEBASE stored as "enniaA 5.0.1" and
"enniaF 6.0.0 (rev ea0da3ed8)R ##D HRINFO0m" for months. It is not an
Evennia quirk and it is not a parser gap: everything those servers said
after their connect screen arrived shredded, with fragments of two replies
overlaid on each other.
RAW[14] Ne, : ekMUD (Name: TrekMUD)
RAW[17] Vsion 6: enniaA.1.0! (Version: Evennia 6.1.0)
RAW[20] RRRRmmand 'WHVERSION scnot available. (two replies in one line)
The cause is ours. Every phase ended in FlushPendingLineAsync, which pushed
a newline through InterpretAsync to shake loose a line the server never
terminated. That is the *inbound* channel — the same one the read loop feeds
— and on an MCCP2 session an inflater sits at the head of it. So the newline
was never delivered to the line buffer at all; it was spliced into the
middle of the peer's deflate stream as though the peer had sent it, and the
inflater's state never recovered.
Ruled out in this order, each with a run rather than an argument:
- Evennia. Captured the raw compressed stream off trekmud.com:1701 with a
plain socket; Python's zlib decodes it perfectly.
- TelnetNegotiationCore's inflater. Fed that same captured stream one byte
at a time through MCCPInflateTransform: 703 of 703 bytes identical to
zlib's output.
- TelnetNegotiationCore generally. A minimal TNC client reads the block
cleanly against the live server, with MCCP alone, with all twelve of this
probe's plugins, with this probe's exact send sequence, and with the same
lock-polling — and it is the same on 2.11.0 and 2.12.0.
- Our plugin set, our builder, our buffer handling. Reduced the probe to a
builder chain identical to that clean client and it still shredded;
replaced the phase logic with fixed delays, same builder, and it stopped.
The fix is not to skip the flush when compressed — that trades one loss for
another, and the guard that keeps a busy DIKU from reading as a measured
zero depends on seeing an unterminated prompt. TelnetNegotiationCore 2.12.0
added PacketPatchProtocol, which infers a prompt boundary from silence on the
interpreter's own byte-processing loop, where the line buffer has exactly one
writer and nothing is pushed into the peer's stream. It retires itself the
moment a server marks a real prompt with IAC GA or IAC EOR. So: take the new
version, register the plugin, and delete the hand-rolled version of it.
A prompt lands in LastPromptBytes rather than through OnSubmit — the library
will not pretend a prompt is a line — so the probe appends it, in the order
taken. Deduplicated by identity, not content: every take allocates a fresh
array, and a server repeating its gate is showing it again, which is what
ARepeatingGateStopsAtTheRoundBound needs to see.
ProbeOptions.PromptHold is 500ms, the library's default and what TinTin++
and Mudlet both use; shorter splits a line at any server that pauses
mid-output. A phase waits it out rather than racing it, and only when
HasPartialLine says something is actually being held — which is false for
the overwhelming majority of settles.
ACompressedSessionIsNotShreddedByOurOwnSettling fails on the old behaviour
and passes on the new; FakeGame gains an AnnouncesMccp mode so the fixture
really compresses. Verified live: all five Evennia games now read
Evennia 6.1.0 / 5.0.1 / 6.1.0 (rev e174e49f5) / 6.0.0 (rev ea0da3ed8).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Say the settle rule once, instead of waiting for it afterwards
The wait for a held line was a step tacked onto the end of every flush,
which read as though the probe were re-implementing something the library
already does. It is not that — it is a rule about when a phase is over — so
it belongs in the one place that decides that.
A phase is not settled while the library is holding an unterminated line.
The two clocks start at different moments: SettleAsync's at the last line,
PacketPatchProtocol's at the fragment that arrived after it. Ours therefore
always expires first, and a phase that ended there would push its own prompt
into the next phase's slice — the misattribution the cursors exist to
prevent. MaxPhase still bounds it, so a server holding a fragment for ever
is not waited on for ever.
Fast() now holds for the same 120ms as its QuietPeriod, mirroring
production's 500/500. The suite previously passed on a margin — an 80ms hold
against a 120ms settle — which is not the ratio that ships and so proved
nothing about it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Take the prompt where the library hands it over, not at a phase boundary
OnPrompt is the hook for this, and the probe was already using it — for EOR
and Suppress Go-Ahead, to record the boolean fact that a server marks its
prompt boundaries. It just never took the text.
All three of the library's boundaries behave the same way: IAC EOR, IAC GA
and PacketPatchProtocol inferring one from silence each take the standing
partial line into LastPromptBytes and then call back, on the interpreter's
own byte-processing loop. None of them submits it — a prompt is not a line
and the library will not pretend otherwise — so nothing reaches OnSubmit.
So all three now hand it to one PromptSink, which appends it to the probe's
line list. That the callback runs on the byte-processing loop is the whole
reason it belongs there: the prompt lands between the lines either side of
it, in the order it was taken, rather than being swept up afterwards at the
next settle. Which in turn deletes the identity-dedupe the sweep needed —
the callback fires once per prompt, so there is nothing to deduplicate — and
the `taken` parameter that was threaded through five signatures to carry it.
This also covers a marked prompt, which the previous version reached only by
accident. TNC 2.12.0 routes IAC GA through TakePartialLineAsPrompt too, so a
server that ends its prompts with GA had exactly the same problem as one
that ends them with nothing, and neither is special-cased now.
FlushPendingLineAsync is what its name says again: wait for the interpreter
to finish processing. The settle rule added in the previous commit stays —
it is about when a phase is over, not about collecting anything.
ACompressedSessionIsNotShreddedByOurOwnSettling still fails on the old
behaviour and passes on the new. Crawl 485 three times over, Catalog 640,
Crawler 332, Discovery 335, Web 1153.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
0 commit comments