Forum teuk.org

🏰 Ghosts in the Common Room, Runes on the Airwaves

in Mediabot · started by TeuK · 7h ago

TeuK · 7h ago

Mediabot MB900–MB917 · Eighteen maintenance rounds, four shared modules, a more useful Partyline, and 30,522 passing assertions across the same 1,156 test files.

The previous article, The Gatekeeper, the Owl Post and the Atlas, ended at MB899. Command admission, remote version retrieval and country lookup had acquired dedicated owners. The old Helpers doors still worked, and the country-lookup chapter had left a precise observation for the next investigation: an asynchronous producer could return the right text, yet the caller would still receive a failure value.

MB900 starts by following that clue.

What followed grew into eighteen rounds of work across process handling, the Partyline, music metadata, RSS delivery, plugin lifecycles, worker payloads, radio integration and Gemini output. Some rounds introduced operator tools. Others changed only a few production lines. Several repaired variations of the same underlying mistake: treating an old callback, connection or registration as if it still owned the current operation.

Another recurring thread was Unicode. Text already decoded into characters, raw UTF-8 bytes, JSON escapes and output budgets each have a proper place. Crossing one boundary twice could lose a title, corrupt a setting after restart, or turn a successful command into a misleading error.

The latest reported development-host run passes 30,522 / 30,522 assertions, followed by all 37 security invariants and startup integrity. The catalogue remains at 1,156 test files. The work stays on 3.6dev, with no database schema change.

The castle has gained a better common room. Several ghosts have also lost permission to keep opening doors after their shift has ended. đź‘»

🦉 MB900 — An empty pipe is not a collected child

The country-lookup observation from MB899 was part of a wider process-lifecycle problem.

Six asynchronous readers used handles created by Perl’s open '-|'. Such a handle has an important closing behaviour: closing it waits for and collects its associated process. Meanwhile, the application already had an explicit asynchronous collector, using process watching or nonblocking wait operations.

Two owners could therefore try to collect the same child. The stream’s automatic close could arrive first and consume the exit status. Alternatively, a producer could close its output but remain alive, leaving the stream’s close waiting inside the IRC event loop.

That distinction explains why this was more than a cosmetic failure value. A worker could have finished writing while still doing something else. End of output did not establish that the process had exited.

The affected paths were:

Operation Existing owner
Country lookup External::Country
Dictionary lookup UserCommands
YouTube search External::YouTube
Forward/reverse resolution command Helpers
Partyline reverse DNS Partyline::SessionAuth
Owner-only Partyline evaluation Partyline::Privileged

The reproduced consequences differed. Country and dictionary lookup could replace valid output with their failure fallback. DNS paths could lose a failing exit status and treat plausible text as successful. YouTube completion could disappear. An Owner evaluation that closed its outputs and continued running could stall the loop despite its watchdog.

The correction gives these readers close_on_read_eof => 0. EOF is still consumed and the reader leaves the loop, while the existing collector remains responsible for the process status. Finalizers close the handles after collection. The evaluation finalizer gains the explicit close it previously received implicitly.

Other workers using ordinary pipe/fork handles retain their existing handling. The correction does not replace every worker implementation or retune timeouts, output bounds, encoding, TERM/KILL escalation or command permissions.

The regression fixture uses real processes, pipes, IO::Async readers, timers and signals. Controlled producers close output early, ignore TERM, or write plausible output and exit with status 2. A heartbeat verifies that the event loop remains responsive.

In the initial country reproduction, the reference returned N/A despite receiving FR. A producer remaining active for four seconds also delayed the heartbeat by roughly four seconds. The candidate returned FR, allowed the heartbeat around 0.05 seconds, and completed the timeout/escalation path around 0.34 seconds. Those are controlled probe observations, not a whole-bot performance benchmark.

Fourteen assertions extend six existing cases. The unresolved atlas-room observation now has a reproduced cause and a targeted repair.

🚪 MB901 — A closed Partyline session must stay closed

Attention then moved to the Partyline, beginning with connection ownership.

Telnet and DCC can receive several complete lines in one read. Previously, a .quit or a flood-triggered disconnect could remove the session while the reader continued dispatching lines already extracted from that buffer. Accessing user state could recreate an empty session entry. A closing stream could receive another authentication prompt, producing warnings about writing to a stream already being closed.

There was a second problem with callbacks retained after a connection ended. They remembered a connection number. If a newer session occupied that number, an old EOF or close callback could remove the replacement’s state, confirmations or console subscription and alter its session metric.

MB901 makes the readers and close callbacks check that the session exists and that its registered stream is the exact stream associated with the callback. Readers repeat the check after every dispatched line. Once a command ends the session, the rest of that old buffer stops there.

Authentication failures that require disconnection now remove session state immediately after requesting delivery of the final disconnect message. They use the existing idempotent cleanup, including its DCC timeout and console-hook handling. A second cleanup does not repeat the departure.

The native fixture uses local socketpairs, real streams and timers, and a private in-memory SQLite login setup. It deliberately replaces session entries to exercise stale callbacks without relying on a lucky operating-system race.

The new matrix passes 32/32, against 10/32 on the reference. Normal login transcripts remain identical after normalizing the clock and descriptor-number fields. Existing permissions, password policy, thresholds and command behaviour retain their owners.

The Fat Lady’s portrait now checks which visitor is actually standing in front of it.

📜 MB902 — Decode each Telnet byte once

A session can remain valid while its incoming protocol framing is still mishandled. MB902 addresses that next layer.

The previous Telnet reader cleaned the complete buffer on every read. Part of that buffer could already contain decoded text from an incomplete line. Escaped IAC data could therefore pass through the filter twice, with results depending on where network reads happened to split.

Two-byte controls were not consistently removed. Telnet subnegotiation content was also mixed with command text: a newline inside protocol parameters could manufacture a line for the login dispatcher.

Mediabot::Partyline::Telnet now owns incremental framing. Each Telnet connection retains a small decoder state and a separate text buffer. Wire bytes are consumed once; partial decoded lines are not fed back into the framing filter.

The decoder handles option-command parameters, escaped IAC data, two-byte controls and subnegotiation through its closing marker. Unknown command pairs keep their historical literal fallback. Subnegotiation content is discarded as it arrives rather than accumulated in memory.

This is a framing filter, not a complete terminal emulator. Existing CR/LF handling and application commands keep their owners. The same 4,096-byte user-line bound remains, while DCC continues using its raw input path. The historical stateless helper call also retains its previous algorithm.

Twenty-four assertions extend the existing Partyline unit case. They cover fragmented UTF-8, literal IAC bytes, negotiation controls, embedded newlines, independent connections, exact line limits and real reader/login paths. A grouped property check tries all 25 split positions across two protocol examples. A separate probe feeds a one-megabyte subnegotiation and verifies that the decoder keeps only its small mode state before resuming normal text.

The candidate passes 24/24 new criteria; the reference passes 11/24. The MB901 lifecycle checks remain in place for both transports.

🛋️ MB903–MB904 — Give the common room better tools

With the input and session boundaries strengthened, the next two rounds add practical Partyline features using existing help text and connection memory.

Find the right spell without reading the whole library

MB903 adds focused help and literal help search. Command matching ignores case and accepts the optional leading dot; the existing DCC alias remains discoverable.

History gains a recent-count selector, literal search and current-session clearing. Its capacity remains ten commands. Searches inspect retained entries; they never execute the commands they find.

Partyline command Result
.help plugins Show help for that exact command
.help search timer Search names, syntax and descriptions literally
.history 5 Show the five most recent retained commands, in chronological order
.history find café Search this connection’s retained history, ignoring case
.history clear Clear this connection’s retained command history
.away Reviewing the logs Mark this connection away with a reason
.back Remove its away state
.whom away / .whom here Select authenticated sessions by presence
.whom telnet / .whom dcc Select authenticated sessions by transport

Help search is bounded to twelve result lines and a 128-character search term. It treats regular-expression metacharacters as ordinary text. UTF-8 comparisons reuse the existing IRC text decoder while preserving the stored and displayed entries.

History inspection does not insert its own requests into history or announce them to peers. Clearing affects the current connection’s short in-memory list; it does not erase server logs or reach into another session. The default .history output remains unchanged.

The help catalogue also loses duplicate entries and corrects descriptions that had drifted from the actual .top, .remind and KV behaviour. Those commands themselves are not rewritten. The existing 62 documented command names and six plugin-help lines remain.

Put an away sign on this chair

MB904 adds Mediabot::Partyline::Presence, owning .away and .back.

Presence belongs to a connection. Two connections using the same account can have different states. Disconnect cleanup discards that connection’s reason, and chatting while away does not silently clear it.

Reasons must be valid UTF-8, exclude control characters, and fit within 160 encoded bytes. That is a wire budget: forty four-byte emoji fit; a forty-first does not. Invalid input leaves the previous state intact and is not announced. A real state change is announced once to authenticated Partyline peers. Repeating the same state only replies to the requester.

.whom gains the filters shown above and displays matching away reasons. Sessions still authenticating remain outside these views. When nobody is away, the original default listing is preserved. Presence is neither sent to IRC nor added to the runtime-status document, and it requires no persistence.

The dispatcher retains authentication and its existing command throttle. MB903 adds 26 behaviour assertions; MB904 adds 27. These acceptance checks describe new functionality as well as compatibility. They are not presented as 53 pre-existing bugs.

Both rounds exercise the actual Telnet/DCC reader paths through local sockets. Fragmented Unicode reaches the wire encoded once, session isolation survives reconnects, and test interfaces reject unexpected database or IRC access during the new commands.

🎵 MB905–MB906 — Read the music’s runes at the right boundary

Once the Partyline work was complete, the review returned to the rest of the bot. Music metadata exposed another boundary problem: a JSON decoder expecting UTF-8 bytes could receive a Perl character string already decoded by the HTTP helper.

Spotify: preserve the title without borrowing the artist’s name

In Spotify’s oEmbed, NEXT_DATA and JSON-LD paths, literal accents or emoji could make valid JSON disappear behind the existing error fallback. The same metadata could work when represented with ASCII Unicode escapes. That made the symptom look like inconsistent upstream metadata.

MB905 gives the three readers an adapter that encodes character input once and leaves raw UTF-8 bytes intact. Invalid JSON still follows the existing error handling.

Display fields receive their own guards. JSON objects, arrays and booleans must not become HASH(...), ARRAY(...) or an accidental 1 in a title, artist or album. A valid entity name can replace an unusable title.

Album fallback needed particular care. After a structured album name was rejected, a broad historical regex could borrow a later artist name from a sibling object. The correction remembers that the album has already been processed structurally and avoids that misleading fallback. It preserves the older unparsed-album path, including nested cover metadata before the album name.

That compatibility case mattered during development: an initially narrower regex would have changed valid legacy behaviour. The final version retains that path and protects it explicitly.

Eighteen assertions extend the existing Spotify case, including five native AsyncWorker completions. A separate comparison checks 51 nominal metadata and request traces. Formatting, the green badge, display bounds, cache/pending identity, HTTP ordering and worker ownership remain.

AppleMusic: share a proven conversion

MB906 applies the same input-contract correction to AppleMusic’s JSON-LD and iTunes readers. An incomplete-looking title card could actually have lost valid artist, year, duration or track-count details during JSON decoding.

The conversion becomes Mediabot::JSON::decode_json_payload. Spotify delegates its existing adapter to it; AppleMusic uses it at the two affected boundaries. The helper retains byte input, encodes character input once, and leaves the underlying decoder’s values and exceptions intact. Each caller keeps its own error policy.

The new module performs no request, owns no cache and introduces no persistence. Other providers keep their current decoding paths because their input contracts have not all been established as equivalent.

Thirteen assertions extend the AppleMusic case, including native workers and cache reuse. Qualification compares 39 decoder vectors and 27 nominal AppleMusic vectors, while retaining all eighteen Spotify scenarios from MB905.

The shared spell is small because its responsibility is precise: establish the decoder’s byte input without changing the caller’s interpretation of the result.

⏳ MB907 — An old RSS timer must not touch its successor

RSS output has a mode that sends one article and schedules later articles with countdowns. A failure while constructing, registering or starting a timer could leave its prepared queue and duplicate index stuck. Future polls then found articles apparently already queued, despite their never having been delivered.

A retained old expiry callback could also act after a mode change, remove the current timer or release another article too early. Cleanup itself could reenter the runtime and install a successor while the old callback still believed it owned the slot.

MB907 gives each countdown a private arm identity and explicit cancelled/completed state. Cancellation revokes ownership before touching the timer. Expiration also revokes ownership before cleanup, then checks whether cleanup installed a replacement before draining anything further.

Retained, cancelled and duplicate callbacks become harmless. Weak references prevent an externally retained callback from keeping an entire RSS runtime alive.

Timer setup failure clears only the affected prepared queue and its duplicate-index entries. It attempts to stop and remove a partially registered notifier and records a bounded diagnostic. Unsent articles have not been acknowledged in SQL, so a later ordinary poll can prepare them again. Recovery does not dump the remaining queue into an immediate burst.

The acknowledgement and drain body, repository SQL, quotas, rotation, formatter and separate paced mode retain their behaviour. Thirteen assertions extend the existing delivery-acknowledgement case. They exercise native spacing, setup failures, retained callbacks, mode changes, inline expiration and cleanup reentrancy.

A separate 24-vector comparison preserves nominal output, acknowledgements and queue/index state across batch sizes, channels and delivery modes. The timer keeps its own appointment book; yesterday’s owl cannot cross out tomorrow’s delivery.

🪪 MB908–MB912 — Give plugin resources an owner, not just a name

Five consecutive rounds examined API v3 plugins. Their common concern was lifetime: a package name can be reused, a command slot can change hands, and a startup hook can fail after doing real work.

The fixes preserve the current instance’s policies while preventing stale objects from impersonating a successor.

MB908 — A replacement package is a different instance

Some guards checked whether a plugin name was enabled. After unload/reload under the same name, that answer could become true again for callbacks belonging to the old object. Old command invocations or jobs could recover access to output even when the replacement had fewer grants.

PluginManager now shares an exact-entry identity check across command handlers, retained event callbacks, job callbacks and their output boundaries. The current registration must be the same entry captured when the resource was mounted.

Multi-channel jobs recheck ownership and activation before each channel handler. If the first handler disables or unloads its plugin, the old fan-out stops instead of continuing through a previously captured channel list.

Current-instance observe/off/disabled behaviour, quarantine and legacy fallbacks remain. Disabling and re-enabling the same registration retains its existing semantics; replacing that registration ends the old callbacks’ ownership.

Fourteen new assertions use the real manager, registry, EventBus, Scheduler and IO::Async loop. A native countdown actually expires, deferred events are drained, and retained callbacks are invoked deliberately. Eighteen nominal vectors compare current-instance behaviour.

MB909 — Old service bridges cannot borrow new grants

Output was only one boundary. RuntimeV3 also created service bridges for HTTP, KV snapshot/commit, quote read/write, factoid read/write and activity reads.

Those eight bridges captured the manager and package name while the old PluginContext retained its grant snapshot. After replacement, an old context could reach a new same-name registration through those callbacks. A context retained from failed mounting had a related route into a later load.

MB909 factors the eight service sinks through one registration-scoped factory. Before delegation, each bridge checks the exact registration identity introduced in MB908. The link is weak, so retaining a context does not keep an unloaded registration alive. An unbound or expired scope fails with a constant diagnostic before reaching its service.

The service signatures, arguments and return conventions remain. Observe mode retains reads and suppresses mutations. Current disabled contexts keep their existing refusals and can work again after their own registration is enabled.

Thirteen assertions exercise the actual loading/context/invocation path with controlled service backends. A separate comparison covers 96 facade calls across twelve nominal vectors.

These are lifecycle guards around trusted Perl plugins. They do not turn plugin code into a sandbox or undo an operation already accepted by a service.

MB910 — Attempt cleanup after a partially completed start

A startup hook can allocate a timer or subscribe a listener and then throw. Previously, the manager marked startup successful only after the hook returned. Its rollback therefore cleaned core jobs but could omit the plugin’s stop hook precisely when partial initialization needed it.

The manager now records that start was attempted before calling it. If activation fails, rollback attempts stop with the same PluginContext after the established job cleanup. If cleanup also throws, the original activation error remains the primary failure.

The hook contract is preserved: an exception signals failure; a false return without an exception keeps its historical meaning. Objects without a start hook do not acquire an inappropriate stop call during that rollback.

Nine new assertions use real native timers, listeners and lifecycle owners, including retry and combined load-plus-enable failure. Cleanup is attempted, not magically guaranteed: plugins still need a stop implementation safe after partial initialization.

MB911 — Teardown must not erase somebody else’s command

Command cleanup had a similar name-versus-identity problem. An old mount descriptor could remove a newer command registered in the same slot. Restoring its saved built-in could overwrite a replacement, or collide when the old name had become another command’s alias.

Each v3 command mount now retains the exact registry entry returned when it was installed. A shared unmount helper removes or restores only a registration it still owns. A different entry survives, even if it has the same plugin label or is encountered through an alias.

When the slot is vacant, the historical built-in fallback can still be restored. Normal owned-command removal remains, and older descriptor forms retain their established handling. The helper is shared by unload and partial-mount rollback, including rollback after a later resource fails to mount.

Ten assertions extend the existing lifecycle case and reuse the MB908 fixture. Twenty-four nominal cycles cover all six bundled v3 packages with empty/full grants and enabled/disabled states. Registry entries, handlers, aliases and cleanup results agree with the reference.

The old caretaker may return the key it was given. It may not remove the new caretaker’s lock.

MB912 — Preserve the runes through a cold restart

The fifth plugin round addresses persistent runtime state.

The writer serialized JSON as characters and printed it to a raw file. Some non-ASCII text was then written as UTF-8 while the historical reader treated bytes as characters. A prefix or channel name could change after restart. Latin-1 could follow a different path, and a character-count check could underestimate the bytes actually written.

MB912 uses JSON::PP’s ASCII escaping for the persisted representation. Unicode values travel as standard JSON escapes, and the existing reader reconstructs the characters. The unchanged 1 MiB limit now measures the exact representation being written.

Escapes can occupy more bytes than raw UTF-8, so a large Unicode document can reach that existing bound sooner. The limit is not raised. ASCII-only documents retain their previous bytes.

The logical state format remains schema 1. Grants, policies, enabled state, private file mode, atomic publication and in-memory rollback remain. The historical reader, including its Latin-1 compatibility, is retained. Existing files are not migrated, and previously damaged text is not repaired automatically.

Eight assertions extend the boot-state case. Seventy-two nominal persist-and-cold-restore cycles compare the six bundled packages across grants, enablement and policy modes, including actual disk bytes and permissions.

Across MB908–MB912, the plugin code acquires a clearer answer to three questions: which registration owns this callback, which registration owns this resource, and what exact bytes represent its settings after restart?

📦 MB913 — Keep the worker’s parcel inside its envelope

The next review widened beyond plugins to heavy-command workers.

CommandAsync collects outgoing message intentions inside a child and replays them from the parent. Its 64 KiB result transport already had an oversized-response fallback: retain the first ten intentions and discard oversized logs.

But ten messages can still be too large. One 70,000-character message was enough. The parent bounded its buffer, received clipped JSON, and could report Database error even though the command had succeeded.

There was another edge case: selecting ten array positions from fewer real intentions could pad the fallback with null entries. Excessive logs could trigger that path after a short successful response, leaving replay to encounter entries that were never messages.

MB913 serializes the normal result unchanged when it fits. Otherwise, it keeps at most ten actual intentions and removes entries from the end until the complete serialized UTF-8 envelope fits the unchanged limit.

That preserves an ordered prefix. It does not cut a message halfway through a character or JSON structure. If the first intention alone exceeds the budget, it and its successors are omitted, and the established truncation notice goes to the caller. The correction does not promise complete delivery of excessive output.

The parent replay, its EOF/reap barrier, locks, delivery context, timeouts and news pacing remain. Only child-side result encoding changes.

Eight assertions extend the existing native delivery-context case and retain its previous 48. They observe actual fork/pipe/Stream/process-watch behaviour, serialized bytes, order, truncation notices, counters and lock release.

A supplementary producer emits a short first message followed by 20,000 emoji, representing 80,000 UTF-8 text bytes. The candidate preserves the first message and reports truncation; the reference reports the misleading database failure. Forty-three nominal worker scenarios preserve normal and already-fitting fallback results.

The Owl Post now checks the parcel’s actual weight, rather than assuming ten objects always fit in one envelope.

📻 MB914–MB916 — Clearer metadata and safer radio failures

Three rounds then followed the radio path from remote status to TCP commands and downloaded-track metadata. The database schema remains untouched throughout.

MB914 — A JSON object is not a station title

Icecast’s public views normalize a snapshot, but optional fields could still contain JSON objects, arrays or booleans. Those values could leak into display text as reference strings or manufacture a false mount identity such as /HASH(...).

A very long digit string could also convert to a non-finite numeric value in a mount counter.

MB914 accepts scalar values at the affected display boundaries and rejects references there. Numeric normalization rejects references before conversion and returns the existing zero fallback when the conversion is not finite.

Valid large finite numbers keep their treatment; no arbitrary ceiling is added. Decimal/comma/zero handling remains. The raw snapshot and unrelated metadata remain available in their original form, while primary-mount selection, ordering and one fetch per public view retain their behaviour. Aggregate-total arithmetic is outside this correction.

Eight assertions extend the existing snapshot case, preserving its 66 earlier assertions. Seventy-two nominal snapshots through six public views provide 432 comparisons. A real loopback HTTP server also exercises malformed metadata, a non-finite counter and a healthy Unicode response through HTTP::Tiny.

MB915 — A broken Liquidsoap socket must reach the error handler

The TCP client could connect and then find its peer gone when printing a command. With the default SIGPIPE action, the process terminated by signal 13 before the write-error return could run. An eval around the operation did not turn that fatal signal into the intended false result.

This also affected the radio request path calling the client directly: it could never reach its established queue-failure message.

The production correction is small: Liquidsoap::command locally ignores SIGPIPE during its operation. A failed write can then follow the existing failure return, diagnostic, socket close and log. Leaving the scope restores the caller’s previous signal handler.

No global signal policy or retry is added. Command framing, quit, response decoding, request IDs including zero, server errors and existing limits retain their behaviour.

Six assertions extend the existing injection-guard case. Native TCP peers deliberately reset connections before the real client writes. Separate processes isolate fatal signals, and tests cover default, ignored and callback signal handlers across five methods.

A further 120 normal exchanges per version compare returned values, sent bytes, logs and socket/signal state. The radio integration probe confirms that a reset reaches its normal failure message while the healthy request remains unchanged.

MB916 — Decode a downloaded track’s JSON once

A final metadata boundary used the opposite arrangement from the HTTP readers: the yt-dlp .info.json sidecar was opened through a UTF-8 decoding layer, then passed as characters to JSON::PP’s byte-oriented decode_json.

Valid accents, Japanese text or emoji could therefore be rejected. The reader returned an empty identifier, Unknown artist and empty title. Download completion then used its stdout or filename fallback despite richer metadata being present in the sidecar.

One production line changes: the sidecar is opened raw. The existing JSON parser receives bytes and performs the UTF-8 decoding once.

The parser, returned fields, artist/creator/uploader/channel precedence, trimming and failure returns remain. Reading rewrites neither the audio file nor its sidecar. Permission preparation, cataloguing and queueing retain their order and SQL.

Seven assertions extend the existing download-completion case, preserving its earlier fifty. Real files exercise literal Unicode, escaped JSON, invalid input and the actual completion path, with only the catalogue and terminal outputs substituted.

Eighty-one previously working sidecars retain identical values and UTF-8 flags. The resulting fix is one line because the correct owner of decoding was already present; the preceding file layer simply needed to stop doing the same job first.

🗺️ MB917 — Ask whether the reply still belongs in this room

The eighteenth round returns to delayed IRC delivery, this time in the public Gemini command.

The command checked +Gemini when it started. Its provider could complete after the connection had been replaced, the bot had left and rejoined the channel, or the capability had been disabled. The callback still handed its answer to the output path. Provider failures and empty-answer errors could take the same stale route.

A response already split into two paced lines introduced another opportunity: the first line could be valid, with the second becoming obsolete before its timer expired.

CommandAsync already had a suitable transport-and-channel binding check. MB917 extracts its two function bodies unchanged into Mediabot::IRC::DeliveryContext, keeping both historical CommandAsync entry points as forwarding trampolines.

The shared module captures the current transport and, when available, the joined-channel runtime and generation. Revalidation checks bot shutdown, transport identity/connectivity, runtime identity and the captured generation. It retains the established offline-fixture behaviour without attaching old work to a connection appearing later.

Gemini captures that context when submitting work. On completion, it checks the context and its strict chanset again before accepting either an answer or an error. The inflight slot is still released, and a duplicate completion remains inert.

PacedOutput gains an optional predicate attached to an individual batch. It is checked before each line, including immediate and zero-delay paths. A false result or exception discards that batch’s remaining chunks, preserving later batches and the existing boundary delay. Gemini uses it to recheck context and +Gemini between lines. Other callers retain their existing unguarded paths.

The scope matters. A line already accepted by Helpers or its separate AntiFlood queue is not recalled. The provider’s work is not cancelled. DeliveryContext provides lifecycle evidence; each consumer still owns its authorization policy.

Nine assertions extend the existing Gemini case, retaining all nineteen earlier checks. The grouped native probe covers 52 scenarios, including successful Unicode output, error/empty replies, retained duplicate completions, eleven context/policy changes, newer Gemini replies behind cancelled batches, independent successors and throwing guards.

A separate comparison preserves 72 nominal pacing traces and 40 existing CommandAsync binding decisions. Another probe passes through the actual Helpers/Output/Sender path to check encoding, NoColors, badwords, metrics and history. A discarded second line adds neither a sent-message metric nor a history entry.

The full suite also caught a test coupled to the old Gemini count: the compact-runner case expected exactly 19/19 in its success summary. It now checks for a positive passed count equal to the total. Its purpose remains, and the runner itself is unchanged.

The Marauder’s Map is useful here because it answers a current question: is this still the same room, reached through the same connection, for work that is still authorized?

đź§© Four shared modules with specific jobs

The eighteen rounds introduce four production modules:

Module Responsibility Consumers and compatibility
Mediabot::Partyline::Telnet Incremental Telnet wire framing Stateful Telnet reader; DCC keeps its raw path and the historical stateless call remains
Mediabot::Partyline::Presence Connection-local away/back state Existing Partyline facade, commands and authenticated dispatcher
Mediabot::JSON Adapt byte/character JSON input to the existing decoder Spotify and the affected AppleMusic readers retain their parsing and failure policies
Mediabot::IRC::DeliveryContext Capture and recheck transport/channel lifetime Existing CommandAsync entry points and the Gemini command

Their value comes from a bounded responsibility. Telnet framing does not acquire authentication. Presence does not require database persistence. JSON input adaptation does not decide which provider field is trustworthy. DeliveryContext does not silently grant a plugin or command permission to speak.

Several other improvements factor private helpers inside existing owners: plugin registration checks, registration-scoped service bridges, command unmount ownership and worker-result encoding. A useful common function does not always need to become another public module.

The same restraint applies to adoption. The JSON helper is used where equivalent input contracts were demonstrated. The delivery guard is introduced into Gemini while preserving other callers’ policies. Shared code follows established semantics rather than forcing unlike paths into one implementation.

đź§Ş Protect the behaviour, and keep the catalogue steady

The last article ended at 30,251 passing assertions. This one ends at 30,522, a rise of 271 across eighteen rounds with no increase in the 1,156-file test catalogue.

That difference is accounted for: 259 focused assertions extend existing cases, and twelve automatic structural checks cover the four new modules. Shared fixtures support the native scenarios where needed; a new maintenance label does not automatically produce a new numbered test file.

An assertion is not a bug count. The Partyline features need acceptance tests, healthy paths need compatibility controls, and one grouped assertion can exercise many input combinations. The numbers describe the checks performed, not a claim that 259 unrelated defects were found.

The qualification work repeatedly asks two separate questions. Can the old version reproduce the failure? Do previously valid operations still agree after the change?

That produces both adversarial probes and nominal comparisons: early EOF with a live child, stale session callbacks, fragmented Telnet controls, old plugin invocations, cleanup reentrancy, over-budget serialized output, reset TCP peers and Unicode in several representations. Alongside those sit unchanged login transcripts, parser/request traces, registry snapshots, persistent files, byte flags, signal handlers and ordinary worker results.

Native tests retain the mechanisms under investigation. Process-lifecycle probes use actual children and collection. Partyline probes use real socketpairs and readers. Timer tests use IO::Async notifiers. Liquidsoap tests use TCP resets. Metadata tests use actual files or a loopback HTTP server. Controlled collaborators replace public services, production databases and final IRC sends so that the failure can be scheduled deliberately.

Mutation tests provide another check on the checks. Separate altered copies remove a guard, double-encode bytes, retain an obsolete index, change signal restoration or delete a successor queue. The new criteria must notice. Passing only the healthy candidate would offer weaker evidence that the targeted failure is protected.

Local full suites are recorded with their failures. The updater rehearsal MB786 repeatedly fails in the local execution environment on reference and candidate. Some rounds also encounter Doctor’s process-identity observation failure: a live PID according to kill 0 lacks the expected numeric /proc entry in that view. Failure sets are not described as identical when a full run differs. Separate comparisons help interpret those observations without turning them into waived tests.

For MB917, the final local candidate records 30,504 passes and one MB786 failure out of 30,505. The reference records the same failing case. The actual development-host acceptance then passes 30,522/30,522, including the updater check, with its installed dependencies.

Every delivered validator retains the full unfiltered server run. Nothing is excluded merely to obtain a green summary.

📚 The O.W.L. ledger: eighteen accepted development-host rounds

The following figures come from the reported server application logs. Each full run covers 1,156 test files, followed by security GO — 37 invariants and startup integrity.

Round Main change Targeted assertions passed Full assertions passed Full elapsed
MB900 EOF and child collection 1,873 30,265 / 30,265 198 s
MB901 Partyline session lifetime 1,947 30,297 / 30,297 199 s
MB902 Incremental Telnet framing 1,971 30,324 / 30,324 199 s
MB903 Focused help and history tools 1,997 30,350 / 30,350 202 s
MB904 Connection presence and whom filters 2,024 30,380 / 30,380 201 s
MB905 Spotify Unicode and scalar metadata 928 30,398 / 30,398 201 s
MB906 Shared JSON adaptation and AppleMusic 941 30,414 / 30,414 202 s
MB907 RSS timer ownership and recovery 1,195 30,427 / 30,427 201 s
MB908 Plugin callback/output ownership 2,679 30,441 / 30,441 202 s
MB909 Registration-scoped plugin services 2,692 30,454 / 30,454 203 s
MB910 Partial-start plugin cleanup 2,701 30,463 / 30,463 204 s
MB911 Command teardown ownership 2,711 30,473 / 30,473 204 s
MB912 Unicode-safe persistent plugin state 2,719 30,481 / 30,481 205 s
MB913 Serialized worker payload bounds 988 30,489 / 30,489 204 s
MB914 Icecast display/numeric contracts 416 30,497 / 30,497 204 s
MB915 Liquidsoap broken-pipe handling 422 30,503 / 30,503 204 s
MB916 UTF-8 download sidecars 451 30,510 / 30,510 204 s
MB917 Shared delivery context and Gemini guards 1,019; 188 30,522 / 30,522 206 s

The MB917 targeted groups share the public Gemini case. Other groups also differ in scope and overlap across rounds. Their totals must not be added as unique coverage.

Elapsed times are observations from successive acceptance runs, not a controlled speed comparison. This chapter concerns correctness, recovery, useful operator features and clearer ownership. It establishes no test-suite speedup.

đź§ł Make the journey reversible

The installation process receives the same attention as the code change.

Packages verify their contents and the expected source preconditions before applying edits. Backups retain original bytes, modes and absent-file states. Incompatible edits and symlink destinations are refused. A failed validator restores the paths written by that attempt, while guarded rollback refuses to overwrite a conflicting later edit.

Rehearsals exercise application, installed-content checks and repeat application without rewriting files. Restoration is checked again independently. The normal acceptance path still requires syntax checks, focused regressions, perl t/test_commands.pl --progress, the security audit and startup integrity before committing.

The source changes add no database migration. Partyline history and presence use connection memory; JSON fixes operate at serialization boundaries; lifecycle guards use existing runtime identities; radio corrections retain the existing SQL contracts. Persistent plugin state keeps its logical format.

These are accepted development-host maintenance rounds. They do not establish a new stable release or deployment to every production instance.

🕯️ Better rooms, fewer ghosts

At MB899, the atlas room had a known loose end. At MB900, following it exposed a process-collection conflict shared by six asynchronous paths. The same care then carried into sessions, timers, plugin registrations and delayed replies: a resource needs an owner whose lifetime can be checked when a callback finally runs.

Unicode work followed a similarly concrete rule. Identify whether the boundary carries characters or bytes, apply the required conversion once, and measure the representation that will actually be written. That repairs metadata and persistent settings without rebuilding the surrounding feature.

The Partyline gains tools an operator can use immediately: focused help, literal history search, local history clearing, away/back and filtered presence. The rest of the bot gains less visible improvements that matter when something goes wrong: an early EOF no longer steals a child status, a timer failure no longer strands unsent RSS articles, an unloaded plugin cannot borrow its successor’s services, and a broken radio socket reaches an ordinary failure return.

The castle is still being maintained room by room. There are now clearer names on four more doors, more precise ownership inside several existing rooms, and evidence that the old routes still lead where their callers expect. The owls keep their order, the runes survive the journey, and the ghosts have fewer callbacks left to haunt. 🦉

You must be logged in to reply.