Mediabot MB889–MB896 · 21 October 2026 · Eight maintenance milestones: six dedicated modules, two concrete bug fixes, and the same 1,156-file test catalogue.
The previous article, New Houses, Shared Spells and an Orderly Owl Post, ended at MB888. Weather, Fortnite, TMDB, Apple Music, Wiktionary and Open Trivia DB had acquired clearer implementation owners. The shared IRC modules had given text budgets, presentation, sending, pacing and timers their own boundaries.
The next inspection followed two corridors. One led through External::URL, where provider metadata still shared a room with the machinery for launching Chromium. The other led back into Helpers, where reading channel archives and comparing versions lived among unrelated utilities.
Eight maintenance milestones have continued that work. Six modules now own those responsibilities. Two reproduced runtime defects have also been corrected: Chromium could retain more pipe output than its configured limit, and an invalid X counter could slip into a reply beside a valid one.
The latest complete development-host validation passes 30,224 / 30,224 assertions across 1,156 files in 185 seconds. All 37 security invariants hold, and startup integrity passes. Relative to MB888, that is 69 additional assertions with no additional numbered test file.
This chapter remains development work on 3.6dev, with the database schema unchanged. The castle has clearer corridors, the browser’s cauldron has a dependable fill line, and the Book of Admittance has found a suitable shelf. 📖
External::URL still contained two different kinds of work. It decided how to interpret a link, but also knew how to launch Chromium, arrange temporary profiles, drain two pipes, enforce deadlines, send termination signals and interpret the child’s exit status.
The process driver is useful to several provider paths. Its implementation now belongs to Mediabot::External::Chromium, with three established entry points: the DOM fetcher, the child-wait helper and the wait-status decoder.
The extraction gives those operations an explicit dependency list and a clear owner for process cleanup. The module can load without URL routing, Helpers, the External facade or AI providers. Historical names remain aliases in URL, and the External facade imports the fetcher from Chromium. Existing provider calls through the URL hook continue to work.
At this stage, URL drops from 1,761 to 1,457 lines. Its 25 remaining function bodies are byte-identical. Both moved wait/status helpers are byte-identical too. In the fetcher, two fixed path expressions become private dependency functions returning the same executable and profile locations. This creates controlled seams for native tests without introducing a new user setting or changing the driver’s defaults.
The surrounding contract travels with the driver: the existing HTTP(S) argument guard, argument-vector execution with the URL last, profile cleanup, concurrent stdout/stderr draining, deadlines, signal escalation, reaping, diagnostics and UTF-8 handling. The guard still runs before executable or profile I/O.
The driver remains synchronous. Its callers retain their established worker dispatch, so moving the process machinery does not move a blocking browser call onto a new execution path inside the IRC loop.
The new shared fixture supplies a small local Perl executable and a private temporary profile location. The real open3, nonblocking pipes, select, reads, waits and TERM/KILL operations still execute.
The checks cover successful Unicode DOM output, stderr larger than a pipe buffer, nonzero exit, termination by signal, empty successful output and invalid UTF-8. Another child closes its output pipes and keeps running while ignoring TERM, forcing the post-EOF deadline and escalation path to finish the job. Refused arguments are checked before any executable or profile access.
Fresh Perl interpreters isolate these probes from process-global SIGCHLD state left by unrelated cases. A separate completion assertion checks that the expected native results actually arrived. The intentional timeout probe spends real time reaching the deadline it checks.
For before/after comparison, the original implementation is loaded from the source snapshot under an isolated package, adapting only the two path dependencies to the same private fixture. Twenty-three scenarios agree on results, encoding flags, arguments, diagnostics and child-reaping state after temporary process identifiers and paths are normalized.
These are native process-driver checks using a controlled executable. They establish more than a mocked return value, while keeping their scope clear: they do not qualify every installed Chromium version or the current behaviour of public websites.
Reviewing the extracted driver exposed a concrete retention bug. Each stream branch checked whether its buffer was below the cap, then appended the entire next read.
That condition allowed the append to cross the limit. A read could contain up to 65,536 bytes, producing as much as 65,535 bytes beyond a positive cap. The defect existed independently in stdout and stderr.
A controlled example made the discrepancy visible: a one-byte stdout cap could still return a complete 33-byte sample DOM. With a seven-byte stderr cap, a diagnostic could contain 500 retained stderr bytes. The diagnostic’s own explanatory prefix is separate from the stream data being limited.
A small private helper now computes the remaining raw-byte allowance and appends only the portion that fits. Both stdout and stderr use it. The retained buffers obey their limits exactly.
Reading continues after the retained buffer fills. This is essential when both pipes are active: a child still writing stderr must be allowed to drain, finish and be reaped. Stopping reads at the storage limit would change a memory-bound correction into a possible blocked-pipe problem.
The existing defaults remain 2 MiB for stdout and 256 KiB for stderr. Option normalization and zero/negative retention behaviour remain. The cap bounds retained stream data; temporary read chunks and other process memory remain separate costs.
The existing UTF-8 failure path also remains. An exact byte cap can end inside a multibyte character. In that case the decoder reports the same failure and returns the retained raw prefix through its established fallback. The change introduces neither replacement characters nor a new whole-response rejection rule. A successful nonempty retained stdout prefix still follows the existing return path.
Four native checks extend the existing case: 200,000 stdout bytes with a 1,000-byte cap; 200,000 stderr bytes with a seven-byte cap; a 17-byte prefix cutting through an accented character; and zero caps on both streams with continued draining and reaping. The first three fail on MB889’s driver. All twelve native conditions pass after correction.
The fixture review found a separate Perl-context problem. A final grep expression in list context could contribute no argument when its result was empty. The following diagnostic label then occupied the callback’s result position. A nonempty label is true, so a failed condition could appear successful.
The conditions now pass explicit scalar results. The strengthened fixture records a named boolean for each native check, including the eight conditions introduced in MB889.
That repair matters to the evidence: retaining an assertion count would be unhelpful if a false condition could still pass through an argument-shift accident. The corrected before/after run reports 9/12 conditions before and 12/12 after.
A broader native comparison records 24 unchanged scenarios and five intentional cap corrections. The driver correction and the fixture correction are documented separately. No numbered test file is added.
With the shared process machinery extracted, provider-specific ownership could continue. Seven Instagram functions move from URL into Mediabot::External::Instagram: link identification, metadata cleanup and extraction, social-description parsing, HTTP retrieval, display composition and the handler’s cache/worker lifecycle.
URL drops from 1,457 to 1,099 lines, with its 18 remaining function bodies unchanged. Its seven historical Instagram names remain aliases; the External facade imports the handler from the new owner.
The extraction preserves Instagram’s existing execution model. With a suitable event loop, retrieval runs through AsyncWorker. Without one, the established synchronous adapter calls the same fetch implementation. The HTTP request keeps its four-second timeout and 1 MiB bound; the worker retains its six-second deadline, termination allowances and 16 KiB result limit.
Pending-request coalescing also keeps its meaning. A second request for work already in flight does not create a second worker or redirect the first request’s eventual delivery. Completion releases pending state and remains protected against duplicate callbacks. A later cache hit uses the caller and channel making that later request.
The ten-minute cache, case-sensitive resource identity, metadata priorities, pink badge, reset bytes and historical truncation call remain. Instagram’s path continues to work without a browser fallback. Login shells, Story handling and incomplete public details retain their existing interpretation.
Loading the owner alone avoids eagerly loading URL or AI providers. Operations needing shared URL utilities resolve those historical symbols at call time, preserving existing overrides of metadata helpers, decoding, fetching and cache identity.
Seven assertions extend the existing Instagram case, including fresh load orders, alias identity and synchronous/worker recovery with Unicode, cache replay and single delivery. Fifty-nine differential scenarios agree before and after. A mutation bypassing the historical fetch hook is rejected by the new guard.
The result is a provider module whose name describes its work, with the asynchronous behaviour and presentation still connected to their established interfaces.
Four Facebook functions move into Mediabot::External::Facebook: URL normalization, HTML/DOM title extraction, URL-derived fallback labels and the retrieval/cache/IRC handler.
URL falls from 1,099 to 827 lines. Its 14 remaining function bodies are unchanged. All four historical URL names remain available, and the facade keeps exposing the same functions from their new owner.
Facebook has a distinct recovery policy worth preserving. Its first HTTP request uses the existing social-crawler identity, an eight-second timeout and a 1 MiB response bound. A useful HTTP title avoids Chromium altogether.
A Reel without useful metadata takes its established immediate URL fallback, avoiding a browser wait. Other supported links can continue through Chromium with the same configurable options and bounds, then use the URL fallback if needed. Browser exceptions retain their existing propagation.
Presentation remains equally specific: white text on blue, the established bracket/reset sequence, description cleanup and duplicate detection. The description keeps its 150-character treatment, and the final text keeps its 300-character treatment. Converting those limits to a different byte-budget policy during extraction would change existing replies, so the character-based rules remain with the provider. IRC transport retains its own protections.
The cache still lasts ten minutes, retains resource-case distinctions and replays for the current requester and destination. Shared HTML decoding, browser access, fallback and cache helpers remain dynamically resolved through their historical URL symbols.
Five existing source inspections follow the new owner. Five behavioural assertions extend the existing Facebook/Reel case, covering load order, aliases, decoder replacement and a complete fetch/browser/fallback/cache sequence. Sixty-eight before/after scenarios agree. Bypassing the legacy normalizer is detected by a deliberately altered candidate.
The extraction makes Facebook’s policy easier to locate without merging it with the different recovery rules of its neighbours.
The next six functions move into Mediabot::External::XTwitter: normalization, title and description extraction, compact counters, URL fallback text and the handler coordinating FxTwitter, Chromium, cache and IRC output.
URL reaches 489 lines, with its eight remaining function bodies unchanged. They cover extraction, shared decoding, generic titles, cache identity/admission and routing. The module keeps useful common work while individual providers acquire their own implementation owners.
X retains its fast path: matching status URLs try FxTwitter first, using the existing six-second HTTP timeout and 512 KiB response limit. Other supported links, or unsuccessful fast-path retrieval, continue through the established browser path.
Its exception handling differs from Facebook’s. The X browser exception is logged and returns an undefined result. A browser result that completes but contains unusable metadata can still reach the URL-derived label. Preserving that distinction avoids changing when a request produces fallback output.
The X badge, title/description precedence, rejection of generic login shells, removal of decorative quotation marks and compact-counter formatting remain. Post text keeps its 200-character rule, including the existing 197-character prefix plus three dots; final text keeps its 300-character rule.
Six aliases remain in URL, and the External facade keeps its four established X entries. Calls to the legacy JSON decoder, formatter, parsers, browser and cache identity remain late-bound. One regression deliberately replaces the old JSON symbol; bypassing that symbol is caught.
Clock ownership receives attention too. Time::HiRes is declared explicitly for precise HTTP-duration measurements without importing its time into the module. Cache age continues using the original whole-second clock. An extraction should preserve the time source as carefully as the cache lifetime.
Five assertions extend the existing X cache case. One hundred and three differential scenarios agree, including normalization, malformed responses, fallback behaviour, counters, cache boundaries, Unicode and old hooks.
Across the browser and three provider extractions, URL has gone from 1,761 to 489 lines. Those 1,272 lines have gained clearer owners; the reduction describes source placement rather than functionality being discarded.
The newly focused X module made a smaller defect easier to inspect. The engagement-statistics block opened when at least one counter passed the positive-integer check. Once inside, both scalar fields were compared numerically, even when the other field had not passed that syntax check.
A good counter could therefore admit a malformed neighbour. Perl could interpret its numeric prefix and emit a warning; the formatter could then reject its spelling and return zero. That produced misleading statistics in an otherwise usable reply.
| Provider fields | Previous statistics | Corrected statistics |
|---|---|---|
likes = "999oops", retweets = 3 |
0 likes, 3 RTs, with a numeric warning |
3 RTs |
likes = 12, retweets = "7oops" |
12 likes, 0 RTs, with a numeric warning |
12 likes |
Each field now independently passes the same eligibility expression already used by the code before comparison and formatting. Only admissible counters enter the list. A usable sibling remains visible.
The correction retains the existing formatter and its override point. It adds no new numeric range, finite-number policy or general scalar grammar. In particular, it reuses the historical digit expression rather than quietly changing its accepted edge cases during a targeted fix.
The rendered message is cached, so the regression checks both its first delivery and its subsequent replay. Three mixed valid/malformed responses add six assertions to the existing fast-path case. Five of those assertions fail against the original implementation; all 29 assertions pass after correction.
An expanded differential probe records 113 unchanged scenarios and nine intentional corrections across 122 scenarios, with no additional differences. The corrected cases keep the valid counter’s formatter call, produce the expected initial reply and replay, and avoid the misleading zero and numeric warning.
Previously cached messages are not purged. The correction applies to replies composed by the corrected code and the cache entries created from them, which then follow the normal expiry policy.
MB894 was integrated and server-qualified with MB895. Its change is included in the combined result below; there is no separate standalone server qualification to add to the ledger.
The review then returned to Helpers. Three functions formed a coherent responsibility inside that large general utility module: identify the configured archive table, choose relevant live/archive sources and collect rows from them.
They now belong to Mediabot::ChannelLog::Reader:
| Function | Responsibility |
|---|---|
channel_log_archive_table |
Apply the existing validation to the configured archive database name |
channel_log_sources |
Choose the active table alone or active plus archive for the requested scope |
channel_log_gather |
Execute the caller’s query template per source, bind its parameters and stream rows to its callback |
The reader accepts caller-owned database handles. It creates no connection, performs no work during loading and can operate without loading Helpers. Existing history, statistics and activity consumers reach it through the three compatibility wrappers retained in Helpers.
Callers still own their SQL, bindings, result merging and presentation. The __CLSRC__ token is replaced with the selected table name as before, preserving SQL templates containing percent signs such as date-format patterns. The extraction adds no SELECT-validation layer or new database-access policy.
Source selection retains the distinction between content and presence. An archive configured to retain presence but no message content should participate in a presence query without being added unnecessarily to a content query. Existing defaults, configuration reads and table-name validation remain.
The reader preserves the established active-first order and the failure semantics that commands depend on:
| Observed condition | Established result retained by Reader |
|---|---|
| Successful query with no rows | Valid empty result |
| Active-table preparation or execution failure | Result unusable; archive is not attempted |
| Reported active-table fetch failure | Result unusable; archive is not attempted |
| Archive failure before delivering any archive row | Keep the usable active result and log the archive failure |
| Reported archive fetch failure after partial rows were delivered | Mark the combined result tainted and clear live_ok |
The last distinction protects the truth of an aggregate. A callback may already have merged archive rows into its counters. Once that has happened, the generic reader cannot promise that the caller still holds a clean active-only result. Marking the result unusable lets the command reject that partial answer.
This is existing behaviour carried into the new owner. Fetch or callback exceptions retain their established propagation, along with statement-finish attempts and diagnostics. The refactor adds no transaction or automatic reversal of callback effects.
The module contains 130 lines, and Helpers falls from 4,875 to 4,780 lines. Its 97 other function bodies and default exports are unchanged. The three wrappers preserve historical names, while archive/source overrides remain resolved at call time. Direct users can load the reader alone; existing consumers keep their extension points.
Five assertions extend the existing archive-completeness case, bringing it from 40 to 45. They check three fresh-interpreter load orders, a complete empty read without Helpers and two legacy-hook paths. A mutant bypassing those hooks fails five assertions, including both new hook checks.
One hundred and forty-three scenarios agree across the reference API, the new wrappers and direct Reader calls. They cover scopes, SQL and binds, callback order, empty results, failures, diagnostics and configuration. Three scenarios use real DBI/SQLite handles with an attached archive entirely in memory. No production table or schema is changed.
Another group inside Helpers could be separated without moving its I/O: the rules that clean, parse and compare version values.
Five functions now belong to Mediabot::Version::Format: the usable-value normalizer, version parser, digit-string comparator, Mediabot-version comparator and historical getDetailedVersion tuple extractor.
The distinction between cleaning and parsing remains useful. The normalizer trims a usable scalar and rejects the empty/Undefined sentinel. It does not by itself establish that a value follows the version grammar. The parser separately recognises Mediabot’s stable and development forms.
The existing ordering remains major version, minor version, stable/development stage and then development-build information. For example:
| Comparison | Existing meaning retained |
|---|---|
3.6dev-20261021_120000 vs 3.6dev-20261021_130000 |
The first development build is older |
3.5 vs 3.5dev-20991231_235959 |
Stable is newer at the same major/minor base |
3.5 vs 3.6dev-1 |
The higher minor base wins |
| An unrecognised version vs a recognised version | No ordering result is invented |
Development-build numbers continue through the existing length-and-string comparison rather than a new numeric conversion. Major/minor handling, accepted separators, zeros and Unicode behaviour are preserved too. The extraction does not turn the project grammar into a different versioning standard.
The detailed-version helper retains its historical argument shape and four-value tuple, including its scalar-context return. These details matter to established callers even when a normal IRC reply looks identical.
The new module loads and calculates without Helpers, HTTP or DBI. Five wrappers keep the historical Helpers names. When their legacy normalizer, parser or digit comparator is defined, the canonical module resolves it at the time of each call. A differential scenario even replaces the parser between the left and right operands, checking that the second operand observes that replacement.
The I/O boundary is deliberately left in place. Helpers still resolves the local VERSION path from its own source location, reads the file, maintains the cached runtime identity and coordinates remote checks. Moving the file-resolution code alongside pure calculations could have changed which directory was treated as the repository root. Its established location remains part of the startup contract.
The 113-line owner reduces Helpers from 4,780 to 4,722 lines. All 95 other function bodies and its default exports are unchanged, including network/worker logic and user-facing version responses.
Six assertions extend the existing startup/version case from 19 to 25. Three check load orders and autonomous use; three check the old normalizer, parser and digit-comparator hooks. Removing those resolutions yields three failures, while the candidate passes all 25.
A separate probe records 2,835 identical scenarios through the old interface and the direct owner: 51 parsing inputs, all 2,601 version pairs, 169 digit-string pairs, six hook/error cases, seven controlled remote responses and one local read from a foreign working directory. It compares tuples, scalar/list returns, encoding flags, warnings, diagnostics, cache state and request options.
The Book of Admittance now has one home for its recognition and ordering rules, with the owls fetching new information still managed by the established I/O code.
The extractions use two related compatibility techniques.
For Chromium and the social providers, historical URL names remain aliases to their implementation owners. The External facade imports the functions it already exposed from those owners. Across these moves, URL retains its 17 optional exports and External its 33 default exports.
For the channel reader and version rules, Helpers retains forwarding wrappers. The new modules can function alone, and they consult the historical helper hooks when those are defined. This keeps standalone use small while preserving late replacements used by existing callers and fixtures.
Fresh-load tests and function-reference identity checks address initialization order and alias ownership. Runtime-hook tests address a different question: whether a replacement installed after loading is still used. Both matter. A correct alias at startup cannot establish that an internal call still crosses a hook later.
The social-provider bodies change their shared calls to resolve the historical URL symbols explicitly and load dependent utilities when needed. This preserves overrides instead of binding permanently to a private implementation during import.
Provider policy also remains local. Instagram’s worker and absence of browser fallback, Facebook’s Reel shortcut and exception propagation, and X’s FxTwitter path and browser-error return remain distinct. Their renderers keep their current limits and colour/reset sequences. A common location for process operations does not require a single recovery or presentation rule for every service.
The same reasoning applies to the new Helpers extractions. ChannelLog::Reader owns collection, while callers own SQL and aggregation. Version::Format owns recognition and ordering, while Helpers retains identity-file and remote-worker orchestration. These boundaries give the next change a precise place to begin.
Qualification combines evidence with different scopes.
Source comparisons establish which function bodies moved and which surrounding bodies stayed unchanged. Where qualification normalizes lazy dependency resolution, that adaptation is identified. A body move and a deliberate behavioural correction have separate evidence.
Differential probes execute the actual algorithms before and after. They compare results beyond visible text: bytes and UTF-8 flags, return contexts, arguments, diagnostics, cache entries, callback order and process state. Clocks are fixed where comparisons need repeatable timestamps. Normalization is limited to recorded test metadata such as temporary paths, source positions, reference addresses and process identifiers.
Native integration retains real process creation, pipe I/O, signals and child reaping for Chromium, and real in-memory DBI/SQLite operations for the reader. Controlled responses make malformed metadata and HTTP failures reproducible without depending on a public service being available.
Mutations and old-version failures test sensitivity. Removing stderr draining is detected by the pipe-pressure probe. Exact-cap cases fail on the old driver. Provider tests reject bypassed fetch, normalization and JSON hooks. Reader and version cases reject missing legacy resolutions. The counter regression exposes the original warnings and misleading statistics.
The permanent suite grows through existing cases. Across these eight milestones, 51 focused assertions are added. The generic structural case contributes another 18 assertions for six new modules. Together they explain the 69-assertion increase while the numbered-file catalogue stays fixed.
The standalone differential probes are qualification tools; their thousands of input combinations are not thousands of new permanent test cases. No full-suite exclusion list, cached pass or removed test hides the new work.
Local results are also kept separate from target-host results. The updater rehearsal MB786 fails in the local execution environment before and after the relevant changes. An additional local process-identity limitation was investigated during MB894 preparation and documented there. These observations were not converted into server exemptions.
Every server run in the ledger below passes the complete remaining suite, followed by security and startup checks. The source audit’s 37 invariants complement those executions; neither sampled equivalence nor an audit count constitutes a proof against every possible regression or security issue.
Each row records the reported development-host qualification. Every row also passes all 37 security invariants and startup integrity. The complete catalogue contains 1,156 files throughout.
| Milestone | Main work | Targeted assertions | Full-suite assertions passed | Full elapsed |
|---|---|---|---|---|
| MB889 | Chromium process-driver extraction | 539 | 30,171 / 30,171 | 180 s |
| MB890 | Exact stdout/stderr retention and fixture correction | 543 | 30,175 / 30,175 | 180 s |
| MB891 | Instagram ownership | 621 | 30,185 / 30,185 | 182 s |
| MB892 | Facebook ownership | 660 | 30,193 / 30,193 | 182 s |
| MB893 | X/Twitter ownership | 704 | 30,201 / 30,201 | 183 s |
| MB894 + MB895 | Independent X counters and channel-log reader | 1,252 | 30,215 / 30,215 | 185 s |
| MB896 | Standalone version grammar and ordering | 458 | 30,224 / 30,224 | 185 s |
MB894’s counter fix is included in the MB895 qualification, giving seven reported complete validations for the eight numbered milestones. The targeted groups differ in scope and overlap; their assertion counts should not be added together as distinct coverage.
Elapsed times are observations from successive runs. These rounds change module ownership and add real failure-path checks; they do not provide a controlled benchmark or establish a full-suite speedup. The final report remains explicit about the work being measured.
The guarded installers check expected contents before writing, save original bytes, modes and missing-file states, and refuse conflicting local edits. Local rehearsals exercise application, repeat application and restoration. On the development host, the normal installation path still requires the full unfiltered suite to succeed; a failing validation triggers recovery for the files written by that attempt.
The new owners form a useful map:
| Module | Responsibility now located there |
|---|---|
Mediabot::External::Chromium |
Browser subprocess I/O, deadlines, signals, status and decoding |
Mediabot::External::Instagram |
Instagram metadata, display, cache and worker lifecycle |
Mediabot::External::Facebook |
Facebook metadata, provider-specific recovery, display and cache |
Mediabot::External::XTwitter |
X/Twitter metadata, FxTwitter/browser recovery, counters and cache |
Mediabot::ChannelLog::Reader |
Existing live/archive source selection and row collection |
Mediabot::Version::Format |
Existing version normalization, grammar, details and ordering |
That map already made review more focused. Browser extraction was followed by an exact-cap correction. X extraction was followed by an independently validated counter fix. Helpers now delegates two coherent responsibilities without moving startup I/O or rewriting history commands.
Clearer ownership, two corrected runtime defects, preserved compatibility routes and a stable test-file catalogue are the outcomes of this validated development work. Mediabot remains on 3.6dev, with the database schema unchanged.
There are more corridors to inspect. These now have labelled doors, dependable connections and enough room for the next maintainer to open the right book first. 🦉
You must be logged in to reply.