Forum teuk.org

πŸ›‘οΈπŸ¦‰ MB702 β€” Taming the Flood Before Wit Speaks 🌊πŸͺ„

in Mediabot Β· started by TeuK Β· 3w ago

TeuK Β· 3w ago

Mediabot v3 just gained another layer of restraint around proactive AI.

MB701 allowed Wit to finally cross the gates and speak on IRC, but only after a long chain of authorization checks.

MB702 adds an important rule before that conversation can even begin:

When a channel becomes too noisy, Wit should stop asking AI providers for answers β€” and if a reply is already on its way, it must still be revoked before it can reach IRC.

This release also fixes a completely separate production issue discovered during the first real Wit rollouts: newly-added channel settings could remain invisible to a running process because a missing database lookup had been cached forever.

The result is a smaller, safer and more predictable proactive-AI runtime.


🌊 A flood circuit breaker before the provider

The new module:

Mediabot::AI::ConversationFloodGuard

maintains lightweight per-channel pressure state in memory.

Its default policy is intentionally simple:

20 public lines
within 10 seconds
β†’ suppress proactive AI for 180 seconds

The state is:

  • isolated per channel;
  • case-folded;
  • RAM-only;
  • bounded to 256 channels;
  • automatically released when the suppression period expires.

Traffic received while a channel is already suppressed does not keep extending the suppression window forever.

That matters because the guard is meant to be a circuit breaker, not a punishment system.


πŸ§™ The provider is stopped before expensive work begins

The important part is where the new guard sits in the pipeline.

Before MB702:

public IRC line
β†’ ConversationPolicy
β†’ provider request
β†’ decision
β†’ emission authorization
β†’ sender
β†’ IRC

With MB702:

public IRC line
β†’ FloodGuard
β†’ inflight gate
β†’ ConversationPolicy
β†’ provider request
β†’ decision
β†’ emission authorization
β†’ sender
β†’ IRC

When flood suppression is active, the result becomes:

action=no_reply
reason=flood_suppression

and no new Anthropic or OpenAI request is submitted.

This is important for three reasons:

  1. the bot does not add more noise to an already noisy channel;
  2. provider quota and latency are not wasted;
  3. the flood decision is made before generated text even exists.

If the flood guard itself fails unexpectedly, the path fails closed:

reason=flood_guard_error

No proactive provider call is allowed through.


πŸ•°οΈ What if the AI request is already in flight?

This is the subtle part.

Imagine the following sequence:

1. Channel is calm.
2. Wit submits an AI request.
3. The channel suddenly floods.
4. FloodGuard opens the circuit.
5. The provider returns the already-generated reply.

Without a second check, that reply could still arrive after the channel had become unsuitable.

MB702 therefore performs another flood-state read at the provider completion boundary.

If suppression became active while the request was in flight:

provider reply
β†’ flood state re-check
β†’ revoked
β†’ never reaches on_candidate
β†’ never reaches ConversationEmission
β†’ never reaches ConversationSender
β†’ never reaches IRC

The second check is side-effect free:

  • it does not record a fake IRC line;
  • it does not extend the suppression duration;
  • it simply asks for the current state.

This closes the timing window between provider submission and provider completion.


πŸ›‘οΈ MB701 safety remains fully independent

MB702 does not replace any of the previous safeguards.

The MB701 layers remain active:

+Wit channel setting
runtime active/connected state
real JOIN generation
late emission authorization
WIT_SEND_ARMED master switch
ConversationSender
independent sender rate limit
final mutable-state revalidation
IRC delivery

The flood guard sits above them.

So an AI reply must now survive both:

channel pressure checks
AND
the entire MB701 emission authorization chain

That separation is deliberate.

A failure or policy change in one layer should never silently weaken another one.


πŸ—ƒοΈ A production bug uncovered during rollout

While rolling Wit out on real Mediabot instances, one particularly interesting issue appeared.

The database contained the new chanset:

Wit
id=24

but on a process that had already been running, this could still happen:

u chanset #Phat +Wit
Undefined chanset Wit

Restarting the bot fixed it.

That was suspicious enough to deserve a proper investigation.

The root cause was in:

Mediabot::Helpers::getIdChansetList()

A missing lookup was being cached as:

undef

So the sequence was effectively:

process starts
β†’ lookup "Wit"
β†’ row does not exist yet
β†’ cache undef

migration adds Wit=24

next lookup
β†’ cache says undef
β†’ database is never queried again

The new database row remained invisible until process restart.


πŸͺ„ Positive cache only

MB702 changes the cache contract:

successful lookup β†’ cache it
missing lookup    β†’ do not cache it

Therefore:

first lookup: missing
β†’ undef, not cached

migration adds Wit=24

second lookup:
β†’ DB queried again
β†’ 24 returned
β†’ 24 cached

Positive cache behavior remains unchanged.

This is deliberately minimal: no TTL, no invalidation framework, no broader cache rewrite.

Just the behavior required to make runtime database additions visible safely.


πŸ§ͺ Regression coverage

MB702 adds four focused regression contracts:

937 β€” FloodGuard foundation
938 β€” runtime provider gate
939 β€” late in-flight flood revocation
940 β€” negative chanset cache refresh

The historical chanset safety test 77 was also strengthened.

The coverage verifies, among other things:

  • threshold and time window behavior;
  • fixed suppression duration;
  • automatic recovery;
  • bounded channel-state memory;
  • per-channel isolation;
  • no provider submission while suppressed;
  • fail-closed flood-guard errors;
  • in-flight reply revocation;
  • safe metadata-only results;
  • negative database lookup retry;
  • positive cache preservation.

🧹 A useful full-suite catch

The first frozen final full suite found one failure:

625_mb407_channels_canonical_lc_key.t

The flood guard used a private member named:

$self->{channels}

That name collided with a much older static sentinel protecting Mediabot’s canonical runtime channel registry.

Functionally, FloodGuard already normalized keys correctly through its own _channel_key() logic.

But weakening the historical sentinel would have been the wrong answer.

Instead, the private state was renamed:

channels
β†’ channel_state

No runtime behavior changed.

That kept the MB407 protection intact while making the new module unambiguous.

After the repair:

625 + MB702 exact regression: 154/154 PASS
targeted suite:              807/807 PASS
fast lane:                  6254/6254 PASS
security audit:             GO β€” 14/14 invariants

πŸ” Security and secret scanning

The current public worktree was scanned with Gitleaks:

Gitleaks 8.18.2
current public worktree:
no leaks found

The full Git history still reports four previously-known Twitter API findings.

They were reviewed separately and classified for this release gate as:

REVIEW_ACCEPTED_TWITTER_API

That historical classification does not weaken the requirement for the current public tree: the release candidate itself is clean.

The built-in Mediabot security audit also passed all 14 invariants.


πŸ¦‰ Real DEV proof

The final MB702 code was loaded into the real DEV process and exercised on IRC.

The master switch was temporarily armed and +Wit enabled on #boulets.

A normal under-threshold sentence produced the complete live path:

[WIT_DRYRUN]
action=consider
reason=eligible

[WIT_EMIT_DRYRUN]
action=emit
reason=authorized

[WIT_SEND]
action=sent
reason=delivered

[WIT_AI_DRYRUN]
action=reply
reason=model_reply
provider=anthropic
model=claude-haiku-4-5-20251001

No:

flood_guard_error
runtime error
unexpected flood suppression

appeared.

The DEV instance was immediately returned to its safe state afterward:

WIT_SEND_ARMED=0
WIT_ENABLED_CHANNELS=0

πŸ§ͺ Final validation

The final candidate was frozen before the last full suite.

VERSION
3.4dev-20260826_115634

Frozen manifest:

b59e4bb2c23c721dd0c045288c3a8096d7d3dbffa43dbea38b0c13a1e60cc30c

The final full suite then completed successfully:

823 / 823 test files
16914 / 16914 tests PASS
RC_FULL=0

After the suite:

frozen manifest unchanged
git diff --check = 0
WIT_SEND_ARMED = 0
WIT_ENABLED_CHANNELS = 0
DEV service = active/running
NRestarts = 0

The exact bytes that passed the full suite were then committed and pushed.


πŸ“¦ Commit

bc1c4b7
MB702 πŸ›‘οΈπŸ¦‰ tame the flood before Wit speaks β€” revoke late replies and refresh new chansets without a restart πŸͺ„πŸŒŠ

Published version:

3.4dev-20260826_115634

🏰 Where this leaves Wit

With MB700, Wit learned how to observe and decide.

With MB701, Wit was allowed to speak β€” but only through a guarded emission path.

With MB702, Wit now also knows when the room is simply too loud.

And, perhaps more importantly, Mediabot now knows that an answer which was safe when it was requested may no longer be safe when it comes back.

That little distinction is exactly the kind of thing that turns a clever bot into a dependable one.

πŸ¦‰ The owl may speak.
🌊 The flood may rise.
πŸ›‘οΈ But the gates still decide what gets through.

You must be logged in to reply.