Version: 3.4dev-20260820_080732
Commit: 620c6d7
Commit message: 🪄 Let Claude Leave the Event Loop Without Losing the Thread
MB675 fixes a problem that was subtle in normal IRC use but important for Mediabot’s runtime architecture:
a Claude / Anthropic request could block the main IRC event loop while the HTTP request was in progress.
The visible AI response was fine.
The problem was what happened while Mediabot was waiting for Anthropic.
MB675 started with a read-only event-loop audit.
Several commands were already protected by asynchronous boundaries:
news / actualites → CommandAsync worker
horoscope → CommandAsync worker
define → internal async path
version → AsyncWorker
But Claude still followed a synchronous path:
IRC dispatch
→ claude_ctx()
→ claudeAI()
→ HTTP::Tiny request()
The Anthropic request had a timeout that could reach tens of seconds.
That made Claude one of the strongest candidates for a real IRC event-loop stall.
Rather than relying only on source inspection, a small direct IRC timing probe was created.
It connects directly to:
teuk.org:6667
without using ZNC or WeeChat.
The probe joined #boulets and sent:
m ai reponds uniquement par MB675PROBE
then only about:
100 ms
later:
m uptime
This avoids WeeChat’s own anti-flood pacing and gives precise millisecond timings.
Observed:
SEND #1 m ai ...
SEND #2 m uptime
SEND GAP 100.266 ms
AI reply : +954.480 ms
uptime reply : +2163.685 ms
Result:
ORDER=AI_BEFORE_UPTIME
uptime itself does not need an external API call.
Yet it was effectively queued behind the Anthropic request.
That confirmed the architectural problem seen in the source:
Anthropic HTTP was running synchronously in the main IRC process.
A tempting solution would have been to wrap the complete command with something like:
CommandAsync::run_ctx_async(...)
and execute the whole Claude command in a child process.
That would have freed the IRC loop, but it would also have moved important Claude state into the child.
Claude maintains process-local state such as:
conversation history
prompt cache
rate-limit state
persona / pin state
metrics
response handling
Updating those structures in a forked child would not reliably update the parent process.
That would solve one problem while quietly breaking conversation continuity.
MB675 therefore uses a narrower async boundary.
Only the Anthropic HTTP transport leaves the main process.
Conceptually:
main IRC process
|
| prepare request
| preserve conversation/history state
|
+----> AsyncWorker
|
| HTTP request to Anthropic
|
+----> result returned
|
| parse result
| update history
| update metrics/state
| emit IRC response
The important distinction is:
network I/O → worker
conversation state → parent
So Mediabot gets a non-blocking event loop without losing Claude’s thread.
Making Claude asynchronous introduces a new possibility that did not really exist while the event loop was blocked:
two AI requests for the same conversation can overlap
That could create inconsistent history ordering.
MB675 therefore also adds a conversation-level in-flight guard.
While one Claude request is running, a second request targeting the same conversation is rejected rather than racing the first one.
The lock is released when the request completes.
This behavior is covered by the new regression tests.
The exact same direct IRC timing probe was run again after the migration.
The probe sent:
m ai ...
and then:
m uptime
only:
100.334 ms
later.
Observed:
uptime reply : +131.524 ms from first command
+31.190 ms from its own send
Claude reply : +932.547 ms
Result:
ORDER=UPTIME_BEFORE_AI
So while Anthropic was still working, Mediabot processed and answered another IRC command almost immediately.
AI ~954 ms
uptime ~2063 ms after its own send
ORDER AI_BEFORE_UPTIME
uptime ~31 ms after its own send
AI ~933 ms
ORDER UPTIME_BEFORE_AI
That is the behavior MB675 was designed to achieve.
A dedicated test was added:
t/cases/859_mb675_claude_async_transport.t
Existing Claude-related tests were also adapted where necessary to reflect the new async transport boundary.
Focused Claude / AI regression:
PASSED : 1018/1018 (3s)
Fast suite:
PASSED : 5304/5304 (65s)
Full suite:
PASSED : 14139/14139 (221s)
All with:
RC=0
MB675 also demonstrates the practical impact of the previous round.
Before the MB674 test-suite acceleration, full validation routinely took around:
768s
The MB675 full suite completed in:
221s
That makes heavy validation practical after every significant architecture change instead of something developers are tempted to postpone.
Main implementation:
Mediabot/External/Claude.pm
Adjusted Claude / async regression tests:
t/cases/220_external_claude_api.t
t/cases/224_external_claude_history.t
t/cases/227_external_claude_callback.t
t/cases/534_mb312_ai_chunk_pacing_nonblocking.t
t/cases/578_mb359_claude_content_blocks_parsing.t
New MB675 coverage:
t/cases/859_mb675_claude_async_transport.t
And the normal:
VERSION
update.
The change is intentionally architectural, not cosmetic.
It preserves:
Claude command syntax
conversation history
persona behavior
prompt/cache behavior
rate limits
response formatting
metrics
IRC output
while changing where the slow external HTTP operation happens.
Before MB675:
Claude talks to Anthropic
→ IRC loop waits
→ other commands wait too
After MB675:
Claude request starts
→ Anthropic HTTP runs outside the IRC event loop
→ Mediabot keeps processing IRC
→ result returns
→ parent updates conversation state
→ Claude replies normally
The bot can now keep talking to everyone else while Claude is thinking.
And, importantly, Claude still remembers what the conversation was about. 🪄
You must be logged in to reply.