Forum teuk.org

🦉 The Clockwork Behind the Common Room

in Mediabot · started by TeuK · 2d ago

TeuK · 2d ago

Mediabot MB857–MB862 · 20 October 2026 · Retired listeners, orderly news, honest numbers, reliable calendar timers, fresh plugin responses and complete DTC links.

The previous chapter ended with MB856: coherent radio observations, restored inch arguments and finite conversion results. Six more maintenance rounds have followed, along with a separate Ansible toolkit for updating the production instances.

This chapter visits the machinery behind everyday IRC replies. An event listener should stop receiving events when it is removed. News should keep its rhythm. A calendar task should have a real next occurrence when it says it is running. A request for fresh content should receive fresh content. And a clipped story should leave a way to read the missing page.

The latest complete validation on teuk.org passed 27,238 tests across 1,152 files. Across MB857–MB862, twelve new regression files add 799 assertions. The database schema remains unchanged. 📜

🔕 MB857 — Quietus for retired event listeners

The EventBus takes a snapshot of its listeners before dispatching an event. That makes subscription changes manageable while callbacks run, but the snapshot exposed a lifecycle mistake.

An early listener could unregister a later listener, yet the removed callback still received the current event from the old snapshot. Nested events could leave the same unwanted final echo in an outer dispatch.

The integration reproduction used the actual PluginManager and ScriptDryRun plugin: replacing the plugin correctly unregistered its observer, but the event already being dispatched still reached that retired observer.

MB857 marks removed subscriptions as revoked and checks that state immediately before invocation. This applies to individual removal, clearing one event and clearing the whole bus, through both ordinary dispatch and dispatch reporting.

The lifecycle rules remain precise:

  • A callback already executing can finish normally.
  • A removed callback waiting in a snapshot is skipped.
  • A newly registered listener does not slip into an older snapshot.
  • Removing an old registration cannot remove its replacement.
  • One-shot listeners retain their single-execution behaviour, including nested dispatch.

Execution counts and reports now reflect the callbacks that actually ran. Exceptions from active listeners remain isolated normally.

Quietus finally means that a retired observer stops speaking. 🪄

📰 MB858 — Every news line gets its proper turn

News delivery has a shared 1.5-second interval. Maintaining that rhythm requires ownership of each timer and queue, particularly when callbacks trigger more work.

The review reproduced three related problems. A retained old timer callback could interfere with its successor and leave extra countdowns attached to the queue. Cleanup could discard a replacement queue created while the old one was being removed. A news batch added from inside a delivery callback could also re-enter delivery before the outer send recorded its completion time, allowing another line too soon.

Each timer now owns one specific scheduling arm and must claim it before doing anything. An expired, cleared or superseded callback cannot claim a newer arm. Retaining an old callback also no longer keeps completed queue and transport state alive.

Cleanup detaches its exact queue before stopping and removing the timer. Fresh work created during that cleanup keeps its own queue. Delivery is serialized, so nested enqueue appends work in FIFO order and respects the same shared interval.

Failed timer setup removes partial registrations, discards the abandoned output and leaves later requests usable. There is no immediate retry burst. Existing capacity limits, expiry, destinations and final connection/channel checks continue to apply.

The tests cover actual IO::Async timers as well as controlled callback replay, cleanup replacement and nested enqueue. The Daily Prophet may bring several pages; it still has to deliver them in order. 🦉

🔢 MB859 — Revelio for calculator digits

The calculator’s token pattern accepted Unicode decimal digits through \d, while the numeric conversion behind it expected ASCII digits. A value could therefore look valid to the parser but become zero or a partially interpreted number during conversion.

That mismatch could produce an apparently successful answer and update calculation history with the wrong result.

MB859 makes the supported decimal and scientific literal alphabet explicit: 0 through 9. Unsupported digit characters cause the whole expression to take the existing private syntax-error path. They cannot become a successful public calculation or overwrite the previous successful history.

For example, an expression using Arabic-Indic digits is refused rather than silently interpreted as an ASCII value. This correction does not add digit transliteration or arbitrary-precision arithmetic. Ordinary ASCII arithmetic, hexadecimal input, precedence, supported functions and established formatting keep working.

The new regressions exercise five Unicode digit families in integer, fraction and exponent positions, then verify the actual command handler’s public/private delivery and history behaviour.

Revelio is useful when a number’s appearance and its interpreted value disagree.

🗓️ MB860 — Calendar clocks with a real next tick

A calendar task could finish an occurrence and fail while constructing its next countdown. That construction happened outside the scheduling arm’s checked error path. The task could remain marked started even though it owned no timer and had no next occurrence; another start could then report success without scheduling anything.

The review also exercised replacement and restart during timer construction, registration, start and expiry cleanup. An older scheduling frame could disturb the newer lifecycle’s timer or retain a failed timer through a reference cycle.

MB860 puts construction and installation under one checked ownership boundary. A provisional timer belongs to its exact task and lifecycle. If setup fails or a replacement takes over, cleanup removes only the provisional timer owned by that attempt.

Expiry releases its timer ownership before native cleanup, then checks the lifecycle again before invoking the occurrence or updating its statistics. A restart established during cleanup therefore survives the older callback.

An ordinary arm failure leaves the registration stopped and available for an explicit retry. A completed occurrence remains counted even if scheduling the next one fails. Schedule resolution, minimum waits, periodic tasks and callback error isolation retain their existing behaviour.

The regressions include a real countdown expiry whose following construction fails, an unrelated timer that remains responsive, and a successful later occurrence after retry. The clock’s face and its mechanism now tell the same story. 🕰️

📨 MB861 — Fresh means fresh for plugin HTTP requests

The plugin HTTP cache distinguished the plugin, URL, requested content type and response-size limit, but omitted the requested cache lifetime.

A response kept for 60 seconds could consequently satisfy another invocation requesting zero cache, including a different channel of the same plugin. A shorter lifetime could inherit an older response from a longer policy. Changing a warmed channel to zero cache did not prevent that cache read.

The normalized lifetime now forms part of cache identity, and reads explicitly require a positive lifetime:

Requested policy Behaviour
Zero lifetime No cache read and no cache write
Positive lifetime Reuse within that policy’s own expiry
Different positive lifetimes Independently dated entries
Equivalent explicit/default lifetime Share the same entry

The existing total cache bound remains 128 entries. Zero-cache requests still pass through normal worker capacity, circuit, HTTP validation and final delivery checks. They cannot force work through an open circuit or publish after plugin revocation.

The integration tests use the real PluginManager, HTTP service and short-content package with different channel policies and a live policy edit. A real fork/pipe worker also reads changed local response data to demonstrate that an uncached invocation obtains the new body while the positive-policy cache remains available.

The owl now checks the date on the message it was asked to carry.

📖 MB862 — Lumos on the missing DTC page

MB849 had already given manual and automatic DTC replies the same ten-message quote budget. MB862 fixes a separate omission inside that display path.

The formatter added the full-quote link when there were too many source lines. However, it also clipped individual formatted lines to 430 UTF-8 bytes. A quote containing one very long dialogue could therefore lose text without offering any link to the complete story.

The formatter now treats byte clipping as omitted content too. When an identified quote loses text, its canonical full-quote link is included within the same message budget.

  • If there is room, the link follows the existing dialogue messages.
  • If the batch is already full, its last two positions become ... and the link, preserving eight dialogue messages under the default ten-message cap.
  • A quote already shortened by line count still gets exactly one link.
  • Complete short quotes acquire no unnecessary link.
  • Unknown quote IDs never produce an invented destination.

The change applies to manual !dtc and !bashfr, including random, ID and search modes, and to automatic Spark DTC announcements. The automatic command marker and manual search heading retain their separate positions.

The byte fitter also stops repeatedly encoding a shrinking long line. It encodes the line once, reserves space for the ellipsis and colour reset, and takes a prefix ending on a complete UTF-8 character. Accents and emoji remain intact within the existing byte bound.

The delivery tests compare automatic output with all manual alias/mode combinations through actual command worker completion. Losing the channel after the first manual dialogue blocks the final link; an automatic transport refusal still stops the batch and consumes the usual cooldown.

Lumos leaves the rest of the story within reach, while the ordinary delivery rules continue to govern every message. ✨

🚂 Ansible — Five production platforms, one departure board

A separate operations package has also been prepared to perform the native !update now update path across the five production instances, from the existing ansible account on teuk.org.

Its scope is explicit:

Production instance Host Network
mediabot teuk.org, local production instance Undernet
mediabot nbot.soyou.rocks Epiknet
ubot ubot.teuk.org Undernet
WitchHunter witch.soyou.rocks Undernet
mediabot mediabot.teuk.org Libera

The development instance is excluded, including when it shares teuk.org with the local Undernet production. Other machines present in the Ansible inventory receive no tasks.

The workflow previews all five targets and pins one exact source commit. Application repeats the prechecks and refuses if the selected upstream target has changed since preview. Updates then run one instance at a time, stopping at the first failure. Instances already at the exact target commit and version are checked without a restart.

Each update runs the installed native updater as the bot’s owner, retaining its archive, state-copy and recovery behaviour. Verification checks the exact instance, installed source, unchanged private configuration and a stable replacement process. The package does not perform a global rollback of productions already updated; a fresh preview supports resuming the remaining targets.

The toolkit passed 23 local tests. Fleet execution remains a separate operational step; no completed five-instance production run has been reported at this checkpoint.

đź§Ş The O.W.L. results

All six maintenance rounds passed their complete host suites on teuk.org:

Round New regression assertions Focused lane Fast lane Complete host suite
MB857 — event revocation 131 1,201 / 1,201 8,319 / 8,319 26,565 / 26,565
MB858 — paced news delivery 56 1,595 / 1,595 8,320 / 8,320 26,622 / 26,622
MB859 — calculator literals 176 1,303 / 1,303 8,321 / 8,321 26,799 / 26,799
MB860 — calendar recovery 111 1,549 / 1,549 8,322 / 8,322 26,911 / 26,911
MB861 — plugin cache policy 93 1,548 / 1,548 8,323 / 8,323 27,005 / 27,005
MB862 — clipped DTC links 232 2,144 / 2,144 8,324 / 8,324 27,238 / 27,238

That is 799 new regression assertions in twelve files. The latest full run covers 1,152 test files. All 37 source security invariants pass, and the generated command inventory matches the source catalogue.

The fast lane remains a development check; the complete host suite is required separately. Maintenance source packages retain isolated previews, private backups and restoration after a failed required check. Staging, commits and service restarts remain explicit steps in that maintenance workflow.

MB857–MB861 have been committed and pushed. MB862 has been applied and has passed every required host check; its final development acceptance and commit follow the usual workflow. These checkpoints describe the verified source work, with production rollout tracked separately.

No database schema migration or private configuration change is required by these six maintenance rounds. Event lifecycles, timers, numeric parsing, cache policy and quote presentation have all received focused corrections while the accepted MetaAI research policy and existing delivery safeguards remain in place.

The common room gets fewer ghostly echoes, steadier deliveries, more honest numbers and a light beside every missing page. 🦉📖

You must be logged in to reply.