Forum teuk.org

🏰 The Pensieve, the Remembrall and a Map Finally Folded

in Mediabot · started by TeuK · 2w ago

TeuK · 2w ago

Mediabot MB918–MB950 · Thirty-three rounds of reliability, media previews, channel diagnostics, conversation, memory and contribution — closing the development roadmap with 31,105 passing application assertions.

The previous article, Ghosts in the Common Room, Runes on the Airwaves, ended at MB917. This chapter broadens the review to the whole bot: database connections, community commands, transactions and timers, followed by the roadmap’s five product directions.

By MB950, that development roadmap is closed. Its deliverables are in place; unfinished field checks remain recorded separately. The latest application run on the development host passes 31,105 / 31,105 assertions across 1,166 test files, all 37 security invariants and startup integrity.

MB950 is a documentation-only closure, installed and pushed as e5931ff, VERSION 3.6dev-20261024_170540. The published stable remains 3.5. These rounds introduce no database schema change and do not publish 3.7.

The castle gains a Pensieve for its operating state, a Remembrall for shared history, and a map that can finally be folded away. 🪄

🔌 MB918 and MB922 — Reconnect the objects, not only the bot

A database wrapper can reconnect successfully while the rest of an application continues using a dead connection. Two rounds repaired concrete examples of that split.

Channel objects must follow their connection owner

The main keepalive already updated the bot’s database handle after a reconnection. Cached Channel objects retained the handle they had received when they were constructed.

Setters, user-level and identity lookups, and channel creation could therefore query a closed handle despite a healthy replacement connection.

MB918 gives Channel an optional reference to the existing database owner. Before a database operation, its private resolver asks that owner for the current connection and updates the local alias. The three real construction paths now supply the owner.

If the owner cannot reconnect, the object clears its stale alias. A later operation consults the owner again and can recover after the outage.

Standalone objects supplied with only a database handle remain supported. In-memory getters stay in memory; no new reconnection policy or SQL retry is introduced.

Persistent bans had the same lifetime problem

ChannelBan was constructed once at startup. Its database handle could outlive the connection it represented.

MB922 applies the same ownership principle to the five storage entry points used to find, add, list, remove and expire persistent bans. They resolve the current handle through the database owner already available from the bot.

This matters particularly for expiration: a stale helper could return no expired bans even though a new database connection was available and the existing expiry pipeline was running.

The SQL clock and moderation rules remain unchanged. A database outage retains the existing unavailable behaviour.

⚖️ MB919–MB924 — Everyday commands should mean exactly what they say

Several small-looking defects had direct consequences for ordinary channel use.

MB919 — A blocked karma vote must actually be blocked

Two problems appeared in the karma path.

First, the private karmawatch notification was sent before the vote’s database write. A failed preparation or execution could therefore tell subscribers that a vote had been received even though it had not been stored.

The notification now follows the successful write and statement cleanup. A later failure to read the updated score does not erase the fact that the vote was written; the notification and the optional score lookup have different responsibilities.

Second, the anti-brigading branch detected a sixth distinct voter targeting the same recipient within thirty seconds, warned about the block, and then still recorded the vote. An unlabelled Perl next applied to an inner bare block rather than the outer loop over vote targets.

The loop now has an explicit label, and the blocked vote skips that loop iteration. Other targets in the same message can still be processed. Existing thresholds and expiry remain.

The protection no longer merely announces that it has closed the gate while allowing the visitor through.

MB920 — Unicode letters are part of words

Trivia and quotegame received decoded character strings from the IRC input path, but their answer boundaries still relied on classes designed around ASCII and older byte-oriented input.

That could accept a short answer inside a larger word. For example, on could be treated as a complete answer inside Āon or onœ. The quotegame’s author masking had the corresponding problem: it could obscure an author’s name embedded inside an unrelated word.

Three expressions now also recognise Unicode letters, numbers and combining marks. Answer validation and masking agree that those characters belong to the surrounding word.

This is a boundary correction, not a new normalisation or case-folding policy. The legacy byte paths, IRC nickname punctuation and ordinary successful-game behaviour retain their rules. A rejected answer keeps the round running and does not award points.

MB921 — A note number must be a whole number

!notes del previously relied on numeric coercion. Inputs such as 2oops, 2.5 or 2e0 could select the second note after Perl converted part of the argument.

The deletion branch now validates a complete ASCII decimal ordinal before arithmetic. Historical accepted forms such as 0002, +2 and surrounding spaces or tabs remain valid. Fractions, exponents, suffixes, control characters and references are rejected.

Very large values are bounded by their significant digits before numeric conversion. A malformed argument cannot become a valid position through truncation or overflow.

Search positions still refer to the owner’s full list, and deletion uses persistent note identifiers.

The existing cache/database deletion path remains nontransactional; the fix prevents ambiguous input from selecting a note.

MB923 — Count an authentication failure once per identity

Login throttling tracks the IRC nickname and the requested account name after their existing lowercase normalisation.

When both names were the same, a failed attempt incremented the same entry twice. Three bad passwords produced a count of six, and a correct password on the fourth attempt could be blocked despite the configured five-failure limit.

The login handler now builds a unique list of those normalised keys. Distinct names retain separate counters; a shared identity is counted once. The five-failure, sixty-second policy, early blocking and reset after successful authentication remain.

This restores the intended limit without weakening it.

MB924 — Choose another quote without hoping for luck

The author-specific quote command tried several random draws when it selected the previously displayed quote. All attempts could select the same position, leaving the command to repeat it despite an available alternative.

The replacement algorithm draws directly from the set of other positions. It maps one random rank around the excluded position, so each alternative has a corresponding rank without a rejection loop.

A single available quote can still repeat. The guarantee of an alternative assumes that the result set remains stable and the follow-up database read succeeds. Existing fallback behaviour remains for concurrent changes or failed reads.

This repair applies to the author-specific path. It does not silently replace the algorithms of every random-quote feature.

🧾 MB928, MB932, MB933 and MB937 — A successful call is not always a completed operation

The database review kept returning to two details of DBI programming.

One is that production uses RaiseError => 0: many failures return a false value instead of throwing. Wrapping unchecked statements in eval does not make those operations reliable.

The other is DBI’s true-zero value, 0E0. It can mean that a statement executed successfully but affected no rows. Whether that is acceptable depends on the operation.

MB928 — Delete an account as one owned transaction

The existing user-deletion command removes channel memberships, hostmasks, seen data and the user record in sequence. It already used a transaction, but it did not check every begin, delete and commit result.

With exceptions disabled, a failed delete could be followed by successful deletes of the other records and a success announcement. A surviving account could lose its associated data. A refused commit could also be reported as a completed deletion.

MB928 checks every stage and only announces success after a confirmed commit. Rollback is attempted only if this command successfully began the transaction. A transaction already owned by its caller is neither committed nor cancelled by the deletion handler.

The original error survives rollback for internal logging; the operator retains the generic failure response.

The two-step confirmation, permissions and deletion scope remain. A successful delete affecting zero associated rows is still legitimate for an account without those links; this is not a blanket rule that every SQL statement must affect a positive number of rows.

MB932 — Preserve achievement identities through merge, import and reset

Three multi-write achievement operations needed the same discipline: profile merging, historical JSON import and the reset of time-based achievements after a timezone change.

They now use a private transaction boundary that verifies ownership, begin and commit. Errors in nested SQL helpers become failures within that write scope, while the original connection error mode is restored afterwards.

A separate merge defect concerned aliases. On a uniqueness conflict, the code updated alias dates without transferring the alias to the surviving profile; subsequent cleanup could then remove it. The conflict branch now transfers ownership while preserving the existing earliest unlock date, maximum progression and alias-observation dates.

Historical import also stops treating a failed profile creation as a record that can simply be skipped. The active JSON file is retained on failure, caches are reloaded after attempted recovery, and archival follows a successful commit. Timezone-reset mirrors and pending work are cleared only after their transaction succeeds.

These changes preserve existing achievement thresholds and identity rules. They protect the operation’s integrity rather than redefining what users have earned.

MB933 — Create accounts and purge channels without partial success

Account creation previously allowed the main USER row to survive a failure to insert its initial hostmask. The helper could return an identifier and callers could announce a successful creation. During first-Owner registration, that partial account could also prevent a clean retry.

The account and optional initial hostmask now belong to one checked transaction. The positive user identifier is captured immediately after the user insert. Callers reach their success path only after commit.

Channel purge had a similar publication problem. A refused commit could still be followed by PART, removal from the live channel registry, timer shutdown and cache cleanup.

Purge now verifies the database operation before publishing those runtime consequences. It retains its archive copy and existing table scope. A caller-owned transaction is refused before writes, and the original error survives recovery logging.

MB937 — Confirm that a quote was actually deleted

Both the historical quote path and the API v3 quote-write service could announce a deletion after a successful execute affecting zero rows.

They now inspect the affected-row result before confirming success. Zero follows the existing not-found outcome; an unknown count is an error; a positive count permits confirmation.

The historical DELETE is also bound to the channel identity from the permission lookup. A controlled concurrent move can no longer turn a channel-scoped permission read into an unrestricted deletion elsewhere.

Read failures remain errors rather than becoming fabricated absence. That distinction also protects the shared v3 duplicate lookup before insertion.

No new transaction is added here. A failure after an already executed DELETE is not presented as a guaranteed undo. The repair ensures that the reported outcome matches what the code can actually confirm.

⏳ MB925–MB927, MB934 and MB935 — Own the appointment before ringing the bell

Timers, reminders and polls all carry promises about future state. Their corrections address different ways that those promises could become stale or partially published.

MB925 — Reload database timers without leaving an orphan

Database timer callbacks called _timer_irc_write as a method on the bot. The guarded helper existed in DBCommands but was missing from the exports imported by the real Mediabot class. A native tick could therefore fail with a missing-method error instead of reaching its connection and control-character guards.

The helper is now imported through the existing mechanism.

The reload path also registered a fresh timer for cleanup only after attaching and starting it. An exception after a partial effect could leave that timer attached, and potentially emitting, even though the reload was rejected and the registry still held the old timers.

The current timer is now included in the rollback set before attachment or start. The previous timers are stopped only after all fresh timers have started successfully. An unsuccessful replacement preserves the old set and cleans the new partial work.

MB935 — Adding one timer requires a publication boundary too

A later review followed the separate addtimer path. It inserted the database row before constructing and starting the native timer. A construction, attachment or startup failure could leave a persistent row that might activate later despite the failed command.

The corrected operation opens its own transaction after validation and duplicate checking. It inserts the row and prepares the native timer while the callback remains inactive. Only a successful commit permits publication in hTimers and activation of the callback.

On failure, the callback is revoked before independent stop/remove attempts and rollback of the operation’s own transaction. Existing timers continue running.

Weak references and exact registry ownership prevent retained callbacks from sending after removal or replacement, or holding the bot alive indefinitely.

The Owner permission, allowed verbs, intervals and CR/LF/NUL output guards remain. This is coordination between existing database and event-loop lifecycles, not a new scheduler.

MB926 — Claim a reminder only if it is still pending

Reminder delivery selects candidates before marking and sending them. Between selection and update, another reader can deliver a reminder, its owner can cancel it, or the record can be deleted.

The old update matched only its identifier. A stale candidate could overwrite cancellation, send a deleted reminder from memory, duplicate another reader’s delivery or recreate a cancelled recurrence.

The claim now includes AND delivered=0, and delivery continues only when the update affects a positive number of rows. The reader that actually changes pending to delivered owns the next step.

This does not establish exactly-once delivery: the existing mark-before-output behaviour can leave a reminder delivered after transport refusal, without automatic requeueing.

MB934 — Snooze the duration that was requested

The snooze command read duration units through independent, unanchored searches. -1h could become a positive hour; 1.5h could become five hours. Repeated units or extra text could be partially ignored. An enormous duration could produce an unrecognised infinite timestamp that the delivery code treated incorrectly.

The complete token must now consist of integer quantities and the supported d, h, m, s units, each used at most once, with a positive representable total. Documented forms such as 1h, 30m, 2h30m and 1d retain their meaning.

Snooze also checks state races. A zero-row update is verified with the same owner and pending-state conditions. An already identical stored message can confirm an idempotent request; a delivered, cancelled or changed record cannot produce a false snooze confirmation.

MB927 — A poll deadline also applies to withdrawing a vote

Voting checked the deadline; unvoting checked only whether the poll was still marked active. Because expiration was lazy, a withdrawal after the deadline could change the winner before another vote happened to close the poll.

Vote and unvote now share the existing expiry check. A late withdrawal closes the poll and preserves the ballots.

The historical boundary remains strict: operations at the exact deadline second are still allowed; those after it expire the poll. No background timer or new scoring rule is introduced.

🎵 MB929 — Apple Music gains carefully matched Spotify and YouTube links

The first substantial media addition starts with a simple channel request: when someone pastes an Apple Music song, offer the same track on Spotify and YouTube using their familiar badges.

The new opt-in setting is:

!chanset #test +AppleMusic
!chanset #test +AppleMusicFallback

The ordinary Apple Music preview arrives first. Each successfully matched service can add one short line with its own badge, artist, title and direct URL. The two extra lines may arrive in either order.

Match a track, not merely similar search words

The matching input comes from an exact song lookup: a recognised Apple song identifier, including an album URL with a selected track. A whole album, playlist or incomplete fallback title does not authorize a guessed match.

Spotify candidates need matching normalised title and artist metadata, a verified direct track URL and a duration within three seconds. At most two candidates are inspected.

YouTube candidates require a direct video identity, service metadata and a duration within the same tolerance. Artist attribution must be supported by the title or the recognised artist-channel form. Explicit audio suffixes are understood, while meaningful version words such as live, cover and remix remain significant.

These are metadata checks. They are not acoustic fingerprinting or proof of a shared ISRC. Some valid alternatives will be rejected, and catalogues can contain different masters with similar metadata.

A missing or ambiguous match produces no invented link. The other service and the original Apple Music preview remain independent.

Keep the badges familiar and the rest readable

The Spotify badge is shared with Spotify’s existing output rather than redrawn separately. YouTube reuses its existing badge. Fixed background colour ends at the badge; text and URLs return to the client’s normal theme.

Each line stays within a 400-byte UTF-8 budget without cutting a character or the direct URL. The ordinary NoColors, filtering and antiflood output path remains authoritative.

The feature uses existing configuration: Tavily for bounded domain-specific searches and the existing YouTube API key for video metadata. It adds no new Spotify credential. Missing service configuration or exhausted limits omits the relevant addition rather than blocking the IRC loop.

HTTP runs in bounded child workers, with limited caches and recipients. Each recipient’s policy, transport and activation epoch are rechecked; off/on cannot revive an older lookup.

Installing the code does not activate every channel. An authorized chanset command can register the new setting in the existing catalogue and enable the requested channel without DDL or migration.

🟠 MB930–MB931 — Reddit previews with identity, detail and restraint

Reddit receives its own provider and its own independent channel setting:

!chanset #test +Reddit

Recognised Reddit links go to that provider rather than also generating a generic URL title. +Reddit is independent of +UrlTitle and defaults to off.

The provider recognises posts, comment links, short links, galleries, several public page forms and Reddit-owned media hosts. Recognition is broader than guaranteed access: private content, deleted material, rate limits, login walls and generic application shells can still make a rich preview unavailable.

A media URL without a parent-post identity does not acquire an invented post title. Binary media is not downloaded merely to render a preview.

A compact card that works on light and dark clients

MB931 changes the badge to [reddit], retaining its black-on-orange IRC colours and an immediate reset. The title is bold, while the metadata uses the client’s normal foreground and background.

An illustrative line looks like this:

(Pablo) [reddit] An aurora above the mountains · r/space · u/Astro · 42 pts · 7 commentaires · 23/10/2026 · image

The labels shown here follow the implementation; the example is not a captured live announcement. The complete formatted line fits within 400 UTF-8 bytes. Lower-priority detail can be omitted to preserve the title and the byte budget.

When the public source supplies them, enrichment adds score, comments or replies, publication date, content type and labels such as spoiler or NSFW. Missing counters are omitted rather than invented as zero.

A comment must remain that comment

The metadata is checked against the exact requested post or comment. The optional static iframe from Reddit’s own embed widget must agree through its wrapper, structured entity and permalink.

A comment preview uses its own prose when available. Its score and reply count are not borrowed from the parent post. When the source only supplies the parent title, the output explicitly identifies it as a comment on that title.

Quoted-only content, code and text art are not flattened into a misleading sentence. Attribution comes from the identified metadata, not a user link found inside quoted material. The publication date belongs to the identified entity, not the time the embed was generated.

Detail is optional; basic metadata should survive

The second request has its own absolute budget. Failure, malformed HTML or a timeout preserves a valid basic preview. A detail-source rate limit still starts the appropriate cooldown while retaining usable basic metadata.

Workers keep HTTP outside the IRC loop and bound concurrency, launch rate, sizes, caches and recipients. Delivery rechecks the flag, activation epoch and IRC context.

This is a public-metadata preview. It does not execute embedded scripts, acquire a login session or bypass Reddit restrictions. A successful probe from one network does not establish availability from every production address.

🔎 MB938–MB940 — Lumos: explain a channel before trying to repair it

After the broad reliability pass, the roadmap’s first direction was administration that an operator can understand.

A feature can be enabled in the database while its runtime is absent. A channel can be configured without being joined. A quota reservation can exist without a successful IRC send. A scheduler can tick while its callback does no useful work.

The diagnostic model keeps those observations separate.

MB938 — A private channel explanation in the Partyline

The new command is:

.diagnose #test

It requires an authenticated Master or Owner session. Its result belongs to the requesting Partyline connection; the request is not broadcast to peers. Existing private history and command throttling remain.

The collector reads the channel identity and fixed chanset catalogue with parameterized SQL, then observes the current IRC transport and conversation runtime state. A dedicated adapter renders the structured result for the Partyline.

Four setting states are explicit:

State Meaning
on The setting is registered and a channel setting row exists
off The setting is registered and that channel row is absent
missing The catalogue entry itself is absent
unknown No reliable observation is available

A missing catalogue entry is not silently translated into a specialised feature’s default. A failed database read does not borrow a stale connection to manufacture a clean answer. A configured channel object is not presented as JOIN evidence.

The report explains feature relationships, including AppleMusicFallback’s dependence on AppleMusic and Reddit’s independent gate. It does not override those features’ own policy decisions.

MB939 — Observe pacing without changing it

The next round extends the same report with RSS pacing, RandomQuote scheduling and the two expected instance scheduler tasks.

This required a genuinely read-only path. Historical status readers can create lock files; a diagnostic must observe without repairing state.

The new shared reader opens only existing regular files, bounds the JSON input and briefly attempts a nonblocking shared lock. It refuses unsafe or incomplete state/lock pairs. It creates no directory, file or lock and changes no permission.

RSS and RandomQuote reuse their native validation and status calculations through explicit diagnostic methods. The collector does not instantiate a missing runtime as a replacement.

Several distinctions are made visible:

  • RSS usage counts reservations, including attempts whose eventual IRC write failed.
  • A RandomQuote pending ticket is not proof that a worker is currently running.
  • An absent channel schedule does not acquire an invented next deadline.
  • Scheduler ticks count callback entries, including no-ops and failures.
  • A state file’s modification time concerns the whole document, not necessarily the last successful action in this channel.

A diagnostic read can therefore be fresh while the business activity it describes is old. That is useful information, not a contradiction to hide.

MB940 — Add retained failures and channel-scoped plugin state

The third diagnostic round adds retained RSS failure evidence, contained scheduler callback exceptions and loaded API v3 plugin observations for the selected channel.

RSS contributes a count and latest retained error timestamp. The query does not select feed URLs, labels or raw error text. A successful poll can clear those fields, so zero means no failure currently retained rather than no failure ever happened.

Scheduler exception counters belong to their current task registration. An old callback generation cannot charge its failure to a replacement. Callback exception counts do not claim to cover every timer setup failure or every false return.

Plugin observations use the existing policy explanation and bounded ledgers. They do not discover packages, load plugins, create services or grant capabilities. The report can show configured mode, active/shadow/blocked explanation, missing grants and matching quarantine evidence.

Only the selected channel’s canonical identity is included. Other channels and instance-wide overflow aggregates stay outside the view. The new observations do not copy private plugin configuration or retain raw exception messages.

The text report is capped at twenty-four lines, including at most five plugin rows and explicit omissions. Retention, unload and restart still limit what evidence exists. The Pensieve shows what has actually been retained; it is not an omniscient history of the castle.

🌐 MB940–MB941 — Bring the explanation to mbweb without widening access

The same structured diagnosis becomes available through channel-specific HTML and JSON routes in mbweb.

The request must pass the current identity and channel-access checks before the publication file is read. Revoked membership, deleted accounts and authorization outages cannot reuse a stale session role as permission to inspect a channel.

The web response is an explicit projection. It includes that channel’s settings, JOIN evidence, pacing, retained RSS summary and relevant loaded v3 plugin entries. It omits global inventories, instance scheduler details, private configuration and other channels’ history.

HTML values are escaped. Protected responses use Cache-Control: no-store. The reader checks channel identity, numeric identifier, format version, strict UTF-8, size and observation time. A report older than 120 seconds is hidden rather than dressed up as current state. File modification time cannot substitute for the report’s own timestamp.

An optional local publication, not a web command channel

The bot’s publisher is off by default. It requires a safe directory and an explicit allowlist of at most eight channels. It observes one channel per five-second scheduler tick and atomically replaces a bounded per-channel JSON file.

A web request reads that publication. It does not invoke a bot command, shell action, SQL editor or external provider to regenerate it.

This separation also makes deployment state important. Source installed in the repository, code loaded by a running process and functionality enabled in production are different facts.

That distinction was encountered directly: after MB940 had been committed, the installed mbweb runtime still lacked the new diagnostics module. Deployment then stopped at its dependency audit.

MB941 — Fix the dependency instead of bypassing the audit

The lockfile update is intentionally narrow: body-parser moves from 2.3.0 to 2.4.0, and its required qs entry moves from 6.16.0 to 6.16.1. The direct dependencies and the deployment audit threshold remain unchanged.

The clean installation reported no audit findings at the time of qualification. The package loaded the production dependencies and ran ten local HTTP compatibility checks around Unicode JSON/forms, compressed requests, invalid payloads, size limits and continued availability after rejection.

Those ten checks are compatibility evidence. They are not presented as an independent reproduction of the precise advisory crash. The security update follows the patched dependency and the recorded audit result.

On the development host, MB941 passed the existing seventy-seven Node tests and the full Perl suite. The subsequent deployment and verification completed, the anonymous diagnostic API returned HTTP 401, and the bot’s development PID remained unchanged during that web deployment.

What the pilot established — and what it did not

The development pilot eventually published #test, with two successive observations read by the native web reader. The expected mbweb startup environment was confirmed after restart.

The attempted follow-up account and production-probe recipes did not complete. They are not counted as proof that real allowed and denied accounts were exercised against the intended production channel.

Those recipes were retired. The final record keeps the remaining production acceptance check explicit without launching another sequence of account-creation or Ansible probe tools. Future Ansible operations must use the dedicated ansible account.

The deployed web code and working development publication are real progress. A completed production rights recipe is still a separate result to obtain.

🕰️ MB936 and MB942–MB945 — A delayed answer must still belong to this moment

Asynchronous work can return correct text for an obsolete context. Several rounds strengthen the points at which that context is captured and rechecked.

MB936 — VDM follows the existing delivery context

VDM already rechecked settings, connectivity and channel presence when a fetch completed. Those facts could all become true again after a reconnect or PART/JOIN, while the result still belonged to the earlier conversation generation.

Manual and Spark-triggered VDM requests now use the shared IRC::DeliveryContext. It captures the transport and, when available, the runtime state and JOIN generation before starting the fetch.

Completion releases the reservation owned by that request, then checks the context before emitting a story or an error notice. Obsolete results are discarded without consuming the anti-repeat history. A shared worker can continue serving other valid recipients.

The native regression runs through the real fetcher, worker, fork, pipes and IO::Async lifecycle with local provider output.

MB942 — Hailo post-editing receives recent context

The volatile context used to correct a Hailo answer now expires each individual line after five minutes. A newly observed message does not refresh every older line.

The provider receives the trigger’s context snapshot filtered again when queued work actually leaves for post-editing. Waiting in the queue cannot keep an expired topic alive. Future-dated entries after a clock reversal are also rejected by the freshness logic.

This does not erase or rebuild the learned Hailo brain. It concerns the short-lived conversational material supplied to the post-editor. Existing command/bot exclusions, line bounds, fallback and typing delay remain.

MB943 — Spark must notice that Mediabot has already spoken

Spark observed incoming conversation, but ordinary bot output was not always visible to it without a server echo. A Hailo or AI response could therefore be followed immediately by a spontaneous Spark contribution admitted under the mistaken impression that the room was still quiet.

The common public-message and ACTION paths now notify the existing Spark observer after local output acceptance. The event contains a timestamped bot_pressure observation, not the message text or a nickname.

Private messages, rejected output and queue admission alone do not become successful public-output observations. A logical message split into fragments contributes one observation for that accepted path, rather than one per fragment.

The event does not add a human participant, create a missing observer or activate Spark. Local transport acceptance is also not claimed as an IRC read receipt.

MB944 — Recheck the room at the final Spark boundary

Admission is only the beginning of a generation. Another bot reply can arrive while Spark is calculating.

The orchestrator now exposes the same relevant pressure checks for delivery, and the runtime and sender consult them before a spontaneous response is published. An observation error refuses that spontaneous output rather than treating missing information as permission.

Spark also uses the existing immediate-only no_defer transport option. Antiflood rejection cannot become a delayed contextual answer that later appears after its moment has passed.

The VDM/DTC multi-line story path is checked before its first line, without letting its own first line suppress the rest of the same accepted batch. The one promised continuation of an already opened Portal/Mosaic interaction keeps its existing treatment and other final guards.

This is not a universal arbitration service and it does not cancel provider work already underway. It strengthens the actual delivery boundary.

MB945 — Reauthorize queued Hailo work before spending its provider slot

Hailo had the complementary queue problem: a request could wait behind another channel, lose permission or its IRC generation, and still be submitted for post-editing. Final delivery might reject it later, but the now-useless provider work had already consumed the slot.

The existing authority check now also runs at handoff. An obsolete request releases its place and allows the next valid channel to progress. Work never submitted is recorded as not requested rather than fabricated as a provider failure.

Hailo’s transport also uses no_defer. A queue acknowledgement is no longer counted as a contextual send or retained as observed conversation. Ordinary callers keep the generic queue behaviour; the change is local to these contextual outputs.

The final authority checks after provider completion and typing delay remain independent. A valid request can still become invalid later.

🎛️ MB948–MB949 — Give channels usable profiles and a faithful rehearsal

The conversation work becomes easier to operate through profiles on the existing chanset command:

!chanset #test profile show
!chanset #test profile quiet
!chanset #test profile balanced
!chanset #test profile animated
!chanset #test profile restore

These profile operations require an identified Master or Owner. Their rights are deliberately stricter than ordinary feature toggles because a profile can also change the Hailo ratio.

Profile HailoChatter Spark SparkAction Hailo ratio
quiet off off off unchanged
balanced off on on unchanged
animated on on on 3% base

The profiles govern spontaneous interventions. They do not enable Hailo’s master gate, explicit-response gate, learning, provider credentials or global Spark arming. Exclusions, adaptive brakes, quotas and delivery checks remain.

Applying a profile is one checked transaction over the three settings and optional ratio. The channel and catalogue are locked and reread; caches are invalidated after commit.

restore provides one guarded return point for the immediately preceding effective application. The point is volatile, tied to the database connection and bounded without silently evicting another channel’s recovery information. A conflicting edit refuses restoration. The settings persist through restart; the in-memory undo point does not.

MB949 — The rehearsal must use the configuration it was given

The existing Spark replay accepted an instance configuration but did not pass its nine pacing options to the orchestrator. Exclusions and command prefix were real; cadence thresholds silently remained at defaults.

A controlled example showed why that was misleading: a configured 300-second momentum pause could still produce a candidate after fifty seconds.

Spark::Config now supplies the same mapping to the live factory and replay. The native orchestrator retains its validation, bounds and defaults. No live frequency or arming setting is changed by the repair.

Replay also accepts a metadata-only accepted-output event to exercise the pressure introduced by MB943. Refused output adds no pressure; an output event alone creates no human audience; text and nickname fields are rejected on that event.

The regression compares with the real runtime factory without starting the bot. It validates configured mechanics, not the quality or reception of generated prose.

📼 MB946–MB947 — Accio: make shared memory something people can revisit

The memory direction adds two complementary journeys: return to a particular day, or find an already saved reference.

Return to a date instead of hoping the random picker finds it

!memory retains its historical bounded random-day behaviour. MB946 adds an explicit calendar date:

!memory
!memory 2024-02-29

The date must exist in the Gregorian calendar. The current channel, archive content policy and thirty-day exclusion remain authoritative. A day in the recent exclusion window does not become available merely because its date was typed explicitly.

The response arrives privately as NOTICE. A missing day is not silently replaced with another random date, and a failed history lookup is described as unavailable rather than as an empty history.

A compact navigation line makes the date reusable:

Explore: !memory 2024-02-29 | !onthisday 02-29 | !yearbook 2024

The links use the instance’s actual prefix and do not execute automatically. Their established destinations remain important: memory and onthisday are private; yearbook is public under its existing rules.

The selected day is reproducible, while a representative excerpt may vary. This is navigation through retained history, not a promise to reconstruct purged conversations.

Find a saved quote or factoid without knowing its identifier

MB947 adds:

!memory find café

It searches saved quote text and factoid keys/values in the current channel. It does not search raw conversation logs or another channel’s data.

Each word is parameterized and SQL wildcard characters are treated literally. Input is bounded to six words and 160 UTF-8 bytes, with a minimum of two characters. Matching case and accents follow the database’s collation.

Each available collection shows at most three references, including an explicit recall command, stored date, attribution and short excerpt. A fourth fetched result is only a signal to narrow the search. The command does not claim an exhaustive global total.

+OnThisDay opens the journey, while Factoids retains its own gate. A disabled or unreadable policy does not quietly authorize that collection. One failed collection can be reported unavailable while the other still answers.

Search results are private and do not increase recall counts. Invoking a proposed command remains an explicit decision with its own rights and output destination: !q view is public and counts a recall; !factoid is private under its existing behaviour.

Two bounded SELECTs limit the data returned to the worker. They do not imply constant database cost for a contains-style text search.

The result is a linked memory of references people actually saved, with a clear path from discovery to recall.

🧰 MB948 — A plugin workshop with a real exit door

API v3 already had substantial capability and lifecycle controls. The contribution direction adds a small first step instead of requiring a new author to begin with a complex package.

The generator creates a manifest, README and minimal greeting module. The package is disabled by default and asks for only irc.reply. Its greeting is typed and bounded. It requests no timer, event subscription, HTTP, storage or SQL capability.

Names are validated, existing destinations are refused and symlink parents are not followed. File creation is exclusive rather than overwriting an existing package.

The offline workshop can be run with:

perl tools/plugin_v3_workshop.pl --check

It generates a temporary package and exercises the real native PluginManager: loaded but disabled, enabled without a channel policy, observe/on/off, a second isolated channel, stop, unload and a retained stale handler.

Replies are captured locally, without IRC, database or live configuration. The generator loads or activates nothing in a running bot.

The supervised development walkthrough then connects those mechanics to the existing Partyline commands. A first plugin is complete only when its author also knows how to turn it off and remove its command.

The workshop’s exit door is part of the lesson, not a footnote.

🚶 MB948 and MB950 — Finish the adoption journey, then close the chapter

The fifth roadmap direction is a connected first-channel guide.

It links the existing Debian installation procedure and configuration wizard to schema checks, compilation, inventory, security, first startup, first-Owner registration, discovery commands, diagnostics, private memory and the plugin workshop.

The walkthrough explains an important first-installation distinction: an empty fresh database is not a failed memory search. It also avoids importing private logs merely to create a more impressive demonstration.

A first-channel guide is a delivered document. A fresh installation executed on the target commit is a separate piece of evidence. Existing CI coverage is relevant, but this work does not invent a new remote CI run.

MB950 — Close the development map without erasing unfinished observations

The final closure records the five delivered directions, confirms MB949 as installed and pushed, and moves the remaining field observations to RELEASE_ACCEPTANCE_3.6DEV.md.

Three checks remain explicit:

  1. A fresh development installation completing the first-channel journey.
  2. Real allowed and denied accounts exercising the web diagnostic on the intended production channel, together with absent and stale publication behaviour.
  3. Observation of conversation profiles in actual channel use before qualifying a production choice.

These remain unvalidated conditions for the relevant deployment decisions, without extending this completed roadmap through more recipe tools.

The closure changes five Markdown files; the normal commit process also updates VERSION. On the development host, the package’s nine installer tests passed, the three closure documents and thirteen local links were checked, and the two README discovery links were verified before commit and push.

No application suite was rerun for this documentation-only lot. Its cited application result is the actual MB949 run, not a newly invented MB950 execution.

The closing commit is e5931ff — “MB950: Nox — close the completed 3.6dev roadmap”.

🧪 What the validation record establishes

Regression cases combine real DBI statements, private SQLite databases, constraints, deterministic races, IO::Async workers and local sockets. Where fixtures adapt MariaDB syntax, they do not qualify production locking or every commit-time network failure. Local lifecycle tests also do not establish public-provider availability or conversational quality.

Mutation checks verify that selected regressions detect a removed fix; normal-path comparisons preserve existing behaviour outside the defect. Hosted systemd/process-namespace limitations were recorded without adding server exclusions. The following results are the reported runs on teuk:

Milestone Passing application assertions Test files
Previous article, MB917 30,522 / 30,522 1,156
Apple Music fallback, MB929 30,612 / 30,612 1,157
Rich Reddit previews, MB931 30,657 / 30,657 1,158
Channel/web observability, MB940 30,719 / 30,719 1,158
Linked memory search, MB947 31,006 / 31,006 1,164
Profiles, workshop and adoption, MB948 31,065 / 31,065 1,165
Faithful Spark replay, MB949 31,105 / 31,105 1,166

The net increase is 583 application assertions and ten files, not 583 bugs. Some structural assertions were replaced with native scenario groups, so a lower count in one round can accompany stronger coverage.

MB941 separately passes seventy-seven Node tests and ten HTTP compatibility checks. MB950 passes nine installer tests and documentary checks only. Those counts are not combined into an invented full-suite result.

Packages retain before/after hashes, guarded backups, reapplication and rollback checks for bytes, modes and initial absence. These complement application tests without proving a complete production rollout.

🗺️ What belongs to the next chapter

The delivered bot is easier to inspect, its saved references easier to find, its spontaneous output better bounded, and its first contribution easier to attempt and remove.

Web writes for RSS/RandomQuote remain separate work, requiring rights, CSRF, bot-side execution and audit. Selective Hailo forgetting still needs an authorized corpus, isolated reconstruction and proven recovery. MetaAI promotion, production plugin deployments and a stable release are separate decisions too.

MB950 is installed and pushed. The MB938–MB949 development roadmap is closed, the application suite most recently passed 31,105 assertions, and the remaining field checks are explicit.

Lumos made the operating state visible. Accio made saved references easier to summon. Nox closes the map for this cycle.

Repository reading

The project documentation contains the detailed contracts: CHANNEL_DIAGNOSTICS, APPLE_MUSIC_FALLBACK, REDDIT_PREVIEWS, CONVERSATION_PROFILES, SPARK_OFFLINE_REPLAY, MEMORY_JOURNEYS, PLUGIN_WORKSHOP and FIRST_CHANNEL_3.6DEV.

The closed roadmap and release acceptance checklist retain the delivery and field status.

Prepared on 24 October 2026 from the supplied source snapshot, maintenance notes and operator records. Repository links follow master; this article records MB950.

You must be logged in to reply.