<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>thepragmaticquant — stillpoint</title><description>When agents delegate work down a chain, nothing can tell when the whole cascade is actually done — stillpoint detects that quiet point, soundly.</description><link>https://thepragmaticquant.com/</link><item><title>When is the swarm actually done?</title><link>https://thepragmaticquant.com/when-is-the-swarm-actually-done/</link><guid isPermaLink="true">https://thepragmaticquant.com/when-is-the-swarm-actually-done/</guid><description>Three services across a network, one dropped task, and a question nobody on the wire can answer: did the work actually get done? A copy-paste demo that exits 0 and prints SUCCESS while the real job dies unseen.</description><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div class=&quot;tldr&quot;&gt;&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — When one AI agent hands work to another, which hands it to a third, no single participant can tell whether the whole chain actually &lt;em&gt;finished&lt;/em&gt;. Each agent only sees its own immediate neighbor say “got it.” Below is a tiny three-program demo you can copy, paste, and run in two minutes: a root caller asks &lt;strong&gt;A&lt;/strong&gt;, A asks &lt;strong&gt;B&lt;/strong&gt;, B answers &lt;code&gt;200 accepted&lt;/code&gt; — and then silently drops the work it just promised. Root prints &lt;strong&gt;SUCCESS&lt;/strong&gt;. Every process exits &lt;code&gt;0&lt;/code&gt;. The system looks healthy and is lying, and &lt;em&gt;nobody in the chain misbehaved&lt;/em&gt;. The reason isn’t a bug you can patch in the demo: once A’s request to B closes, A &lt;em&gt;literally cannot&lt;/em&gt; learn B’s downstream fate. There is no global observer of the cascade. This has a name distributed-systems researchers have studied for forty years — &lt;strong&gt;distributed termination detection&lt;/strong&gt; — and the agent ecosystem is building swarms as if it doesn’t exist.&lt;/p&gt;&lt;/div&gt;
&lt;p&gt;Picture a relay race where you, the coach, can see the first runner take off but not the finish line. The first runner passes the baton, jogs back, and tells you “handed it off, all good.” You write down &lt;em&gt;race complete&lt;/em&gt;. But the runner three legs downstream just tripped, dropped the baton, and walked off the track. Your first runner doesn’t know that. You don’t know that. The only report you got — “I passed the baton” — was completely honest and completely useless for the question you actually care about: &lt;em&gt;did anyone cross the finish line?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Swap runners for AI agents and that is the exact shape of a problem the agent industry is racing past. An orchestrator agent delegates to a specialist; the specialist delegates to a tool-running sub-agent; that one delegates again. Everyone reports back to the agent directly above them, and everyone reports the truth. And the system as a whole can still have no idea whether the work got done.&lt;/p&gt;
&lt;p&gt;Let me make that concrete enough to run on your laptop.&lt;/p&gt;
&lt;span class=&quot;marginnote&quot; role=&quot;note&quot;&gt;”Hands work to each other” is the load-bearing phrase. One agent calling one tool is easy. A &lt;em&gt;chain&lt;/em&gt; of agents delegating onward is where knowing-when-it’s-done quietly becomes impossible.&lt;/span&gt;
&lt;h2 id=&quot;three-programs-that-lie-to-each-other-politely&quot;&gt;Three programs that lie to each other politely&lt;a class=&quot;heading-anchor&quot; href=&quot;#three-programs-that-lie-to-each-other-politely&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The entire system fits in three scripts. &lt;strong&gt;Root&lt;/strong&gt; is a script. &lt;strong&gt;A&lt;/strong&gt; and &lt;strong&gt;B&lt;/strong&gt; are two tiny web servers on two different ports — separate processes, talking over the network, exactly like real agents do. Pure &lt;a href=&quot;https://fastapi.tiangolo.com/&quot;&gt;FastAPI&lt;/a&gt; and &lt;a href=&quot;https://www.python-httpx.org/&quot;&gt;httpx&lt;/a&gt;, no agent frameworks, nothing private. You need only &lt;code&gt;pip install fastapi uvicorn httpx&lt;/code&gt; — &lt;em&gt;uvicorn&lt;/em&gt; is the tiny web server that runs A and B.&lt;/p&gt;
&lt;p&gt;(If &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; isn’t your daily language: read &lt;code&gt;await&lt;/code&gt; as “wait here for this.” That’s enough to follow every line.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;b.py&lt;/code&gt;&lt;/strong&gt; — the agent that accepts work and then drops it:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;python&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# b.py  --  run with: uvicorn b:app --port 8002&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; asyncio, logging&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; fastapi &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; FastAPI&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;app &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; FastAPI()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;_running &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;()   &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# anchor background tasks so they can&amp;#39;t be silently garbage-collected&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#D2A8FF&quot;&gt; do_the_real_work&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;    # The actual job B promised to do. Pretend it is a long task.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; asyncio.sleep(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; RuntimeError&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;B&amp;#39;s worker died here&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    except&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;        logging.exception(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;WORK DROPPED -- and no one upstream is listening&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#D2A8FF&quot;&gt;@app.post&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;/work&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#D2A8FF&quot;&gt; work&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;    # Kick off the real work in the background, do NOT wait for it...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;    t &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; asyncio.create_task(do_the_real_work())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;    _running.add(t)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;    t.add_done_callback(_running.discard)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;    # ...and immediately answer the caller: &amp;quot;accepted!&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;status&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;accepted&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;}          &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# HTTP 200, instantly&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;a.py&lt;/code&gt;&lt;/strong&gt; — the middle agent that trusts B’s “accepted”:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;python&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# a.py  --  run with: uvicorn a:app --port 8001&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; httpx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; fastapi &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; FastAPI&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;app &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; FastAPI()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#D2A8FF&quot;&gt;@app.post&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;/run&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#D2A8FF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; with&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; httpx.AsyncClient() &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; client:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;        resp &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; client.post(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;http://localhost:8002/work&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;    # A sees B&amp;#39;s 200 &amp;quot;accepted&amp;quot; and considers ITS OWN job done.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;status&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;done&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;downstream&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;: resp.json()}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;root.py&lt;/code&gt;&lt;/strong&gt; — the orchestrator that believes A:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;python&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# root.py  --  run with: python root.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; httpx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;resp &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; httpx.post(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;http://localhost:8001/run&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;Root got:&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;, resp.json())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;SUCCESS&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; resp.json()[&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;status&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; &amp;quot;done&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; &amp;quot;FAILURE&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Start the two servers in two terminals, then run the root in a third:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;bash&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#FFA657&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; fastapi&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; uvicorn&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; httpx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#FFA657&quot;&gt;uvicorn&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; b:app&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; --port&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; 8002&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;      # terminal 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#FFA657&quot;&gt;uvicorn&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; a:app&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; --port&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; 8001&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;      # terminal 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#FFA657&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt; root.py&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;                 # terminal 3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Root’s terminal prints:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;text&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Root got: {&amp;#39;status&amp;#39;: &amp;#39;done&amp;#39;, &amp;#39;downstream&amp;#39;: {&amp;#39;status&amp;#39;: &amp;#39;accepted&amp;#39;}}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;SUCCESS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;SUCCESS&lt;/code&gt;. Exit code &lt;code&gt;0&lt;/code&gt; (the success code). Meanwhile, moments later, in B’s terminal, you will see a line like &lt;code&gt;WORK DROPPED -- and no one upstream is listening&lt;/code&gt; followed by a traceback — because B already answered &lt;code&gt;200&lt;/code&gt; before the work ran, so when the work blows up, the failure has nowhere to go. The job you asked for did not happen, and every single participant believes everything is fine.&lt;/p&gt;
&lt;figure class=&quot;full&quot; data-astro-cid-bj3fsypb&gt; &lt;div class=&quot;svgfig svgfig-diagram&quot; role=&quot;img&quot; aria-label=&quot;Three boxes left to right: Root, A, B. Solid black arrows show Root&apos;s request to A and A&apos;s request to B, and the 200 &apos;accepted&apos; replies flowing back. A dashed red arrow leaves B downward and falls off the page, labeled &apos;work B dropped&apos;. A green SUCCESS tag sits on Root.&quot; data-astro-cid-bj3fsypb&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; width=&quot;920&quot; id=&quot;fig-06-5-toy-demo-topology&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 920 430&quot; role=&quot;graphics-document&quot; style=&quot;background-color: transparent;&quot;&gt;&lt;style&gt;#fig-06-5-toy-demo-topology [fill=&quot;#1a1d23&quot; i]{fill:var(--bg)}#fig-06-5-toy-demo-topology [stroke=&quot;#1a1d23&quot; i]{stroke:var(--bg)}#fig-06-5-toy-demo-topology [fill=&quot;#18181b&quot; i]{fill:var(--bg-panel)}#fig-06-5-toy-demo-topology [stroke=&quot;#18181b&quot; i]{stroke:var(--bg-panel)}#fig-06-5-toy-demo-topology [fill=&quot;#2c2f37&quot; i]{fill:var(--current-line)}#fig-06-5-toy-demo-topology [stroke=&quot;#2c2f37&quot; i]{stroke:var(--current-line)}#fig-06-5-toy-demo-topology [fill=&quot;#f4f5f7&quot; i]{fill:var(--fg)}#fig-06-5-toy-demo-topology [stroke=&quot;#f4f5f7&quot; i]{stroke:var(--fg)}#fig-06-5-toy-demo-topology [fill=&quot;#8a8f98&quot; i]{fill:var(--comment)}#fig-06-5-toy-demo-topology [stroke=&quot;#8a8f98&quot; i]{stroke:var(--comment)}&lt;/style&gt;
&lt;style&gt;
#fig-06-5-toy-demo-topology{font-family:var(--mono);}
#fig-06-5-toy-demo-topology .box{fill:var(--bg-panel);stroke:var(--comment);stroke-width:2px;}
#fig-06-5-toy-demo-topology .nodelbl{fill:var(--fg);font-size:21px;font-weight:600;}
#fig-06-5-toy-demo-topology .sub{fill:var(--comment);font-size:13.5px;}
#fig-06-5-toy-demo-topology .req{stroke:var(--comment);stroke-width:2.5px;fill:none;}
#fig-06-5-toy-demo-topology .reply{stroke:var(--comment);stroke-width:2px;stroke-dasharray:5 4;fill:none;}
#fig-06-5-toy-demo-topology .mlabel{fill:var(--comment);font-size:14px;}
#fig-06-5-toy-demo-topology .replylbl{fill:#56B4E9;font-size:14px;font-weight:600;}
#fig-06-5-toy-demo-topology .drop{stroke:#D55E00;stroke-width:3.5px;stroke-dasharray:9 6;fill:none;}
#fig-06-5-toy-demo-topology .droplbl{fill:#D55E00;font-size:15px;font-weight:600;}
#fig-06-5-toy-demo-topology .boundary{stroke:var(--current-line);stroke-width:1.5px;stroke-dasharray:3 4;}
#fig-06-5-toy-demo-topology .boundlbl{fill:var(--comment);font-size:12.5px;font-style:italic;letter-spacing:.3px;}
#fig-06-5-toy-demo-topology .ok{fill:#009E73;font-size:16px;font-weight:700;letter-spacing:.5px;}
#fig-06-5-toy-demo-topology .okbox{fill:none;stroke:#009E73;stroke-width:2px;}
&lt;/style&gt;
&lt;defs&gt;
&lt;marker id=&quot;ah-g&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;8&quot; markerHeight=&quot;8&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#8A8F98&quot;/&gt;&lt;/marker&gt;
&lt;marker id=&quot;ah-r&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;8&quot; markerHeight=&quot;8&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#D55E00&quot;/&gt;&lt;/marker&gt;
&lt;/defs&gt;

&lt;!-- the three nodes --&gt;
&lt;g&gt;
&lt;rect class=&quot;box&quot; x=&quot;40&quot;  y=&quot;150&quot; width=&quot;170&quot; height=&quot;86&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;125&quot; y=&quot;188&quot; text-anchor=&quot;middle&quot;&gt;Root&lt;/text&gt;
&lt;text class=&quot;sub&quot; x=&quot;125&quot; y=&quot;212&quot; text-anchor=&quot;middle&quot;&gt;orchestrator&lt;/text&gt;

&lt;rect class=&quot;box&quot; x=&quot;375&quot; y=&quot;150&quot; width=&quot;170&quot; height=&quot;86&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;460&quot; y=&quot;188&quot; text-anchor=&quot;middle&quot;&gt;A&lt;/text&gt;
&lt;text class=&quot;sub&quot; x=&quot;460&quot; y=&quot;212&quot; text-anchor=&quot;middle&quot;&gt;middle agent&lt;/text&gt;

&lt;rect class=&quot;box&quot; x=&quot;710&quot; y=&quot;150&quot; width=&quot;170&quot; height=&quot;86&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;795&quot; y=&quot;188&quot; text-anchor=&quot;middle&quot;&gt;B&lt;/text&gt;
&lt;text class=&quot;sub&quot; x=&quot;795&quot; y=&quot;212&quot; text-anchor=&quot;middle&quot;&gt;worker agent&lt;/text&gt;
&lt;/g&gt;

&lt;!-- Root -&gt; A request + reply --&gt;
&lt;line class=&quot;req&quot;   x1=&quot;210&quot; y1=&quot;176&quot; x2=&quot;371&quot; y2=&quot;176&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;mlabel&quot; x=&quot;290&quot; y=&quot;167&quot; text-anchor=&quot;middle&quot;&gt;POST /run&lt;/text&gt;
&lt;line class=&quot;reply&quot; x1=&quot;371&quot; y1=&quot;210&quot; x2=&quot;214&quot; y2=&quot;210&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;replylbl&quot; x=&quot;290&quot; y=&quot;228&quot; text-anchor=&quot;middle&quot;&gt;200 &quot;done&quot;&lt;/text&gt;

&lt;!-- A -&gt; B request + reply --&gt;
&lt;line class=&quot;req&quot;   x1=&quot;545&quot; y1=&quot;176&quot; x2=&quot;706&quot; y2=&quot;176&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;mlabel&quot; x=&quot;625&quot; y=&quot;167&quot; text-anchor=&quot;middle&quot;&gt;POST /work&lt;/text&gt;
&lt;line class=&quot;reply&quot; x1=&quot;706&quot; y1=&quot;210&quot; x2=&quot;549&quot; y2=&quot;210&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;replylbl&quot; x=&quot;625&quot; y=&quot;228&quot; text-anchor=&quot;middle&quot;&gt;200 &quot;accepted&quot;&lt;/text&gt;

&lt;!-- B&apos;s dropped work, falling off the page --&gt;
&lt;path class=&quot;drop&quot; d=&quot;M 795 236 C 793 310, 800 352, 806 396&quot; marker-end=&quot;url(#ah-r)&quot;/&gt;
&lt;text class=&quot;droplbl&quot; x=&quot;818&quot; y=&quot;300&quot;&gt;B&apos;s work:&lt;/text&gt;
&lt;text class=&quot;droplbl&quot; x=&quot;818&quot; y=&quot;320&quot;&gt;accepted,&lt;/text&gt;
&lt;text class=&quot;droplbl&quot; x=&quot;818&quot; y=&quot;340&quot;&gt;then lost&lt;/text&gt;

&lt;!-- network boundary B sits behind --&gt;
&lt;line class=&quot;boundary&quot; x1=&quot;672&quot; y1=&quot;95&quot; x2=&quot;672&quot; y2=&quot;360&quot;/&gt;
&lt;text class=&quot;boundlbl&quot; x=&quot;676&quot; y=&quot;112&quot;&gt;network boundary&lt;/text&gt;

&lt;!-- SUCCESS verdict on Root --&gt;
&lt;rect class=&quot;okbox&quot; x=&quot;38&quot; y=&quot;95&quot; width=&quot;178&quot; height=&quot;34&quot; rx=&quot;5&quot;/&gt;
&lt;text class=&quot;ok&quot; x=&quot;125&quot; y=&quot;118&quot; text-anchor=&quot;middle&quot;&gt;SUCCESS &amp;mdash; exit 0&lt;/text&gt;
&lt;/svg&gt;&lt;/div&gt; &lt;figcaption data-astro-cid-bj3fsypb&gt;The whole demo: three programs, one dropped task, and a SUCCESS that is not true. The solid arrows are the requests and replies; the dashed red arrow is the work B accepted and then dropped, falling out of everyone&apos;s view.&lt;/figcaption&gt;  &lt;/figure&gt; &lt;script type=&quot;module&quot; src=&quot;https://thepragmaticquant.com/vercel/path0/src/components/Figure.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;The line &lt;code&gt;t = asyncio.create_task(do_the_real_work())&lt;/code&gt; is the whole trick: it starts the work running but does not wait for it — Python schedules it to run later and immediately moves on to return the &lt;code&gt;200&lt;/code&gt;. That single line is the gap between “accepted” and “done,” and it is everywhere. Swap &lt;code&gt;create_task&lt;/code&gt; for a queue, a background thread, or a message to another service, and the trap is identical: nearly every async API in the world answers &lt;em&gt;accepted&lt;/em&gt; and lets you &lt;em&gt;assume&lt;/em&gt; completed.&lt;/p&gt;
&lt;p&gt;That gap is the crux. What each reply actually licenses you to believe:&lt;/p&gt;

























&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;What the reply says&lt;/th&gt;&lt;th&gt;What it actually means&lt;/th&gt;&lt;th&gt;What it does &lt;strong&gt;NOT&lt;/strong&gt; mean&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;200 / accepted&lt;/code&gt;&lt;/td&gt;&lt;td&gt;B received the request&lt;/td&gt;&lt;td&gt;B finished, or even started, the work&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A returns &lt;code&gt;done&lt;/code&gt;&lt;/td&gt;&lt;td&gt;A heard B say &lt;code&gt;accepted&lt;/code&gt;&lt;/td&gt;&lt;td&gt;the work below A succeeded&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Root prints &lt;code&gt;SUCCESS&lt;/code&gt;&lt;/td&gt;&lt;td&gt;every reply came back&lt;/td&gt;&lt;td&gt;the job got done&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The work is gone. The system is green. &lt;strong&gt;No one lied.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;why-a-genuinely-cannot-know&quot;&gt;Why A genuinely cannot know&lt;a class=&quot;heading-anchor&quot; href=&quot;#why-a-genuinely-cannot-know&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you go looking for a bug in the code, you won’t find one — not in the usual sense. Each node behaved correctly given what it could see. The failure is in the &lt;em&gt;shape&lt;/em&gt; of the system, not in any one node’s code.&lt;/p&gt;
&lt;p&gt;Walk the timeline. A opens an HTTP request to B. B accepts it and returns &lt;code&gt;200&lt;/code&gt;. That response &lt;strong&gt;closes the connection.&lt;/strong&gt; From that instant, A and B share no channel. The work B does next happens on B’s side of a boundary A can no longer see across. A asking “did B finish?” after the request closed is like asking “is the runner still running?” after they have left the stadium — there is no wire left to carry the answer.&lt;/p&gt;
&lt;p&gt;You could patch &lt;em&gt;this&lt;/em&gt; demo (have B wait for the work, or call A back when it is done). But every patch just moves the boundary. Make B call A back, and now B can’t see whether &lt;em&gt;A&lt;/em&gt; finished relaying that result up to Root. Add a C downstream of B and the &lt;strong&gt;frontier&lt;/strong&gt; — the set of still-active hops in the cascade — runs off the edge of everyone’s vision again. The structural fact survives every patch.&lt;/p&gt;
&lt;figure class=&quot;full&quot; data-astro-cid-bj3fsypb&gt; &lt;div class=&quot;svgfig svgfig-diagram&quot; role=&quot;img&quot; aria-label=&quot;Four boxes in a row: Root, A, B, C. Gray arrows labeled &apos;settled&apos; connect Root to A and A to B. A thicker vermillion arrow labeled &apos;frontier (still live)&apos; connects B to C. Below, three dashed blue &apos;vision&apos; boxes each span only one node and its immediate neighbor — none covers the whole row — and a vertical dashed &apos;network boundary&apos; line sits between A and B, already crossed by the frontier. The frontier marches right, past every vision box; the system is done only when it is empty.&quot; data-astro-cid-bj3fsypb&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; width=&quot;920&quot; id=&quot;fig-06-5-cascade-frontier&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 920 380&quot; role=&quot;graphics-document&quot; style=&quot;background-color: transparent;&quot;&gt;&lt;style&gt;#fig-06-5-cascade-frontier [fill=&quot;#1a1d23&quot; i]{fill:var(--bg)}#fig-06-5-cascade-frontier [stroke=&quot;#1a1d23&quot; i]{stroke:var(--bg)}#fig-06-5-cascade-frontier [fill=&quot;#18181b&quot; i]{fill:var(--bg-panel)}#fig-06-5-cascade-frontier [stroke=&quot;#18181b&quot; i]{stroke:var(--bg-panel)}#fig-06-5-cascade-frontier [fill=&quot;#2c2f37&quot; i]{fill:var(--current-line)}#fig-06-5-cascade-frontier [stroke=&quot;#2c2f37&quot; i]{stroke:var(--current-line)}#fig-06-5-cascade-frontier [fill=&quot;#f4f5f7&quot; i]{fill:var(--fg)}#fig-06-5-cascade-frontier [stroke=&quot;#f4f5f7&quot; i]{stroke:var(--fg)}#fig-06-5-cascade-frontier [fill=&quot;#8a8f98&quot; i]{fill:var(--comment)}#fig-06-5-cascade-frontier [stroke=&quot;#8a8f98&quot; i]{stroke:var(--comment)}&lt;/style&gt;
&lt;style&gt;
#fig-06-5-cascade-frontier{font-family:var(--mono);}
#fig-06-5-cascade-frontier .box{fill:var(--bg-panel);stroke:var(--comment);stroke-width:2px;}
#fig-06-5-cascade-frontier .nodelbl{fill:var(--fg);font-size:19px;font-weight:600;}
#fig-06-5-cascade-frontier .edge{stroke:var(--comment);stroke-width:2.5px;fill:none;}
#fig-06-5-cascade-frontier .live{stroke:#D55E00;stroke-width:4px;fill:none;}
#fig-06-5-cascade-frontier .stagelbl{fill:var(--comment);font-size:13px;}
#fig-06-5-cascade-frontier .frontlbl{fill:#D55E00;font-size:14px;font-weight:700;letter-spacing:.4px;}
#fig-06-5-cascade-frontier .visionlbl{fill:#56B4E9;font-size:11.5px;font-weight:600;}
#fig-06-5-cascade-frontier .vision{fill:none;stroke:#56B4E9;stroke-width:1.5px;stroke-dasharray:5 4;}
#fig-06-5-cascade-frontier .boundary{stroke:var(--current-line);stroke-width:1.5px;stroke-dasharray:3 4;}
#fig-06-5-cascade-frontier .boundlbl{fill:var(--comment);font-size:12px;font-style:italic;}
#fig-06-5-cascade-frontier .green{stroke:#009E73;stroke-width:3px;fill:none;}
#fig-06-5-cascade-frontier .greenlbl{fill:#009E73;font-size:13.5px;font-weight:600;}
#fig-06-5-cascade-frontier .caption{fill:var(--comment);font-size:13px;}
#fig-06-5-cascade-frontier .marker-dot{fill:#D55E00;}
&lt;/style&gt;
&lt;defs&gt;
&lt;marker id=&quot;ah-g&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;8&quot; markerHeight=&quot;8&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#8A8F98&quot;/&gt;&lt;/marker&gt;
&lt;marker id=&quot;ah-r&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;9&quot; markerHeight=&quot;9&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#D55E00&quot;/&gt;&lt;/marker&gt;
&lt;/defs&gt;

&lt;!-- four nodes along one row --&gt;
&lt;g&gt;
&lt;rect class=&quot;box&quot; x=&quot;40&quot;  y=&quot;150&quot; width=&quot;130&quot; height=&quot;66&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;105&quot; y=&quot;190&quot; text-anchor=&quot;middle&quot;&gt;Root&lt;/text&gt;
&lt;rect class=&quot;box&quot; x=&quot;300&quot; y=&quot;150&quot; width=&quot;130&quot; height=&quot;66&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;365&quot; y=&quot;190&quot; text-anchor=&quot;middle&quot;&gt;A&lt;/text&gt;
&lt;rect class=&quot;box&quot; x=&quot;560&quot; y=&quot;150&quot; width=&quot;130&quot; height=&quot;66&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;625&quot; y=&quot;190&quot; text-anchor=&quot;middle&quot;&gt;B&lt;/text&gt;
&lt;rect class=&quot;box&quot; x=&quot;790&quot; y=&quot;150&quot; width=&quot;110&quot; height=&quot;66&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;845&quot; y=&quot;190&quot; text-anchor=&quot;middle&quot;&gt;C&lt;/text&gt;
&lt;/g&gt;

&lt;!-- settled (gray) edges behind the frontier, live (red) edge at the front --&gt;
&lt;line class=&quot;edge&quot; x1=&quot;170&quot; y1=&quot;183&quot; x2=&quot;296&quot; y2=&quot;183&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;stagelbl&quot; x=&quot;233&quot; y=&quot;174&quot; text-anchor=&quot;middle&quot;&gt;settled&lt;/text&gt;
&lt;line class=&quot;edge&quot; x1=&quot;430&quot; y1=&quot;183&quot; x2=&quot;556&quot; y2=&quot;183&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;stagelbl&quot; x=&quot;493&quot; y=&quot;174&quot; text-anchor=&quot;middle&quot;&gt;settled&lt;/text&gt;
&lt;line class=&quot;live&quot; x1=&quot;690&quot; y1=&quot;183&quot; x2=&quot;786&quot; y2=&quot;183&quot; marker-end=&quot;url(#ah-r)&quot;/&gt;
&lt;circle class=&quot;marker-dot&quot; cx=&quot;690&quot; cy=&quot;183&quot; r=&quot;5&quot;/&gt;
&lt;text class=&quot;frontlbl&quot; x=&quot;738&quot; y=&quot;172&quot; text-anchor=&quot;middle&quot;&gt;FRONTIER&lt;/text&gt;
&lt;text class=&quot;frontlbl&quot; x=&quot;738&quot; y=&quot;205&quot; text-anchor=&quot;middle&quot;&gt;(still live)&lt;/text&gt;

&lt;!-- clipped vision boxes: each node sees only one hop ahead --&gt;
&lt;rect class=&quot;vision&quot; x=&quot;56&quot;  y=&quot;236&quot; width=&quot;258&quot; height=&quot;40&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;visionlbl&quot; x=&quot;60&quot; y=&quot;261&quot;&gt;Root&apos;s vision&lt;/text&gt;
&lt;rect class=&quot;vision&quot; x=&quot;316&quot; y=&quot;236&quot; width=&quot;258&quot; height=&quot;40&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;visionlbl&quot; x=&quot;320&quot; y=&quot;261&quot;&gt;A&apos;s vision&lt;/text&gt;
&lt;rect class=&quot;vision&quot; x=&quot;576&quot; y=&quot;236&quot; width=&quot;234&quot; height=&quot;40&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;visionlbl&quot; x=&quot;580&quot; y=&quot;261&quot;&gt;B&apos;s vision&lt;/text&gt;

&lt;!-- network boundary the frontier has crossed --&gt;
&lt;line class=&quot;boundary&quot; x1=&quot;525&quot; y1=&quot;96&quot; x2=&quot;525&quot; y2=&quot;290&quot;/&gt;
&lt;text class=&quot;boundlbl&quot; x=&quot;529&quot; y=&quot;112&quot;&gt;network boundary&lt;/text&gt;

&lt;!-- the rule restated along the bottom --&gt;
&lt;text class=&quot;caption&quot; x=&quot;40&quot; y=&quot;322&quot;&gt;The frontier marches RIGHT, past every vision box. No node can see all of it at once.&lt;/text&gt;
&lt;line class=&quot;green&quot; x1=&quot;40&quot; y1=&quot;344&quot; x2=&quot;180&quot; y2=&quot;344&quot;/&gt;
&lt;text class=&quot;greenlbl&quot; x=&quot;194&quot; y=&quot;349&quot;&gt;&quot;done&quot; = frontier empty &amp;mdash; a global fact no one node observes&lt;/text&gt;
&lt;/svg&gt;&lt;/div&gt; &lt;figcaption data-astro-cid-bj3fsypb&gt;The frontier is the live edge of the cascade — the hops still in flight. As Root→A→B→C delegates outward, it marches right, past every node&apos;s vision box and past the network boundary. &quot;Done&quot; means the frontier is empty — a global fact no single node can observe, because no node sees the whole frontier at once.&lt;/figcaption&gt;  &lt;/figure&gt; 
&lt;p&gt;That distinction is the spine of the whole problem:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;local fact&lt;/strong&gt; is something one node can see for itself. A &lt;strong&gt;global fact&lt;/strong&gt; is something true of the whole cascade. No node in a delegation chain can directly observe a global fact — it can only see its own edges.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Look at the right-hand column: every row that actually answers “are we done?” reads &lt;strong&gt;nobody&lt;/strong&gt;.&lt;/p&gt;



































&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Fact&lt;/th&gt;&lt;th&gt;Kind&lt;/th&gt;&lt;th&gt;Who can see it directly&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;”I sent my request”&lt;/td&gt;&lt;td&gt;Local&lt;/td&gt;&lt;td&gt;The sender&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”I got a &lt;code&gt;200&lt;/code&gt; back”&lt;/td&gt;&lt;td&gt;Local&lt;/td&gt;&lt;td&gt;The caller on that one hop&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”My immediate child accepted”&lt;/td&gt;&lt;td&gt;Local&lt;/td&gt;&lt;td&gt;The parent of that hop&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”B’s background work succeeded”&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Global&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Nobody&lt;/strong&gt; — it happens after the hop closed&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”The entire A→B→C cascade has settled”&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Global&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Nobody&lt;/strong&gt; — no participant sees all the edges&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Every row a node &lt;em&gt;can&lt;/em&gt; see is local. The information each agent holds is true and insufficient, all at once.&lt;/p&gt;
&lt;h2 id=&quot;what-each-node-can--and-cant--see&quot;&gt;What each node can — and can’t — see&lt;a class=&quot;heading-anchor&quot; href=&quot;#what-each-node-can--and-cant--see&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The reason no clever logging fixes this is that the knowledge is &lt;em&gt;partitioned by design&lt;/em&gt;. Each participant holds one slice of the truth and none holds the union:&lt;/p&gt;

























&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Node&lt;/th&gt;&lt;th&gt;Knows for certain&lt;/th&gt;&lt;th&gt;Is blind to&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Root&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;”A returned &lt;code&gt;done&lt;/code&gt;.”&lt;/td&gt;&lt;td&gt;Whether B (or anyone below) actually did the work.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;”B returned &lt;code&gt;accepted&lt;/code&gt;.”&lt;/td&gt;&lt;td&gt;Whether B’s accepted work ran, dropped, or died.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;”I accepted the work; it later crashed.”&lt;/td&gt;&lt;td&gt;That Root already declared &lt;code&gt;SUCCESS&lt;/code&gt; and stopped listening.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Stack those rows and the gap is visible: there is no column for “the whole system,” because &lt;strong&gt;there is no node standing where the whole system is visible.&lt;/strong&gt;&lt;/p&gt;
&lt;figure class=&quot;full&quot; data-astro-cid-bj3fsypb&gt; &lt;div class=&quot;svgfig svgfig-diagram&quot; role=&quot;img&quot; aria-label=&quot;A left-to-right chain: Root arrow to A arrow to B, with B&apos;s accepted work falling off the right edge into a red zone. Each node has a dashed blue &apos;vision box&apos; around only itself and its immediate neighbor&apos;s reply: Root&apos;s box covers only A; A&apos;s box covers only B&apos;s 200; B&apos;s box covers its own crashed job but has no arrow back to anyone. A faint gray dashed box labeled &apos;the observer that does not exist&apos; is drawn around all three and crossed out — no single vantage point contains the whole cascade.&quot; data-astro-cid-bj3fsypb&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; width=&quot;920&quot; id=&quot;fig-06-5-no-global-observer&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 920 470&quot; role=&quot;graphics-document&quot; style=&quot;background-color: transparent;&quot;&gt;&lt;style&gt;#fig-06-5-no-global-observer [fill=&quot;#1a1d23&quot; i]{fill:var(--bg)}#fig-06-5-no-global-observer [stroke=&quot;#1a1d23&quot; i]{stroke:var(--bg)}#fig-06-5-no-global-observer [fill=&quot;#18181b&quot; i]{fill:var(--bg-panel)}#fig-06-5-no-global-observer [stroke=&quot;#18181b&quot; i]{stroke:var(--bg-panel)}#fig-06-5-no-global-observer [fill=&quot;#2c2f37&quot; i]{fill:var(--current-line)}#fig-06-5-no-global-observer [stroke=&quot;#2c2f37&quot; i]{stroke:var(--current-line)}#fig-06-5-no-global-observer [fill=&quot;#f4f5f7&quot; i]{fill:var(--fg)}#fig-06-5-no-global-observer [stroke=&quot;#f4f5f7&quot; i]{stroke:var(--fg)}#fig-06-5-no-global-observer [fill=&quot;#8a8f98&quot; i]{fill:var(--comment)}#fig-06-5-no-global-observer [stroke=&quot;#8a8f98&quot; i]{stroke:var(--comment)}&lt;/style&gt;
&lt;style&gt;
#fig-06-5-no-global-observer{font-family:var(--mono);}
#fig-06-5-no-global-observer .box{fill:var(--bg-panel);stroke:var(--comment);stroke-width:2px;}
#fig-06-5-no-global-observer .nodelbl{fill:var(--fg);font-size:20px;font-weight:600;}
#fig-06-5-no-global-observer .vision{fill:#56B4E9;fill-opacity:0.07;stroke:#56B4E9;stroke-width:1.8px;stroke-dasharray:6 4;}
#fig-06-5-no-global-observer .visionlbl{fill:#56B4E9;font-size:12.5px;font-weight:600;}
#fig-06-5-no-global-observer .req{stroke:var(--comment);stroke-width:2.5px;fill:none;}
#fig-06-5-no-global-observer .reply{stroke:var(--comment);stroke-width:2px;stroke-dasharray:5 4;fill:none;}
#fig-06-5-no-global-observer .replylbl{fill:var(--comment);font-size:13px;}
#fig-06-5-no-global-observer .crash{fill:#D55E00;font-size:14px;font-weight:600;}
#fig-06-5-no-global-observer .xmark{stroke:#D55E00;stroke-width:4px;}
#fig-06-5-no-global-observer .ghost{fill:none;stroke:var(--comment);stroke-width:1.6px;stroke-dasharray:2 5;opacity:0.6;}
#fig-06-5-no-global-observer .ghostlbl{fill:var(--comment);font-size:13px;font-style:italic;opacity:0.85;}
#fig-06-5-no-global-observer .ghostx{stroke:var(--comment);stroke-width:2px;opacity:0.7;}
#fig-06-5-no-global-observer .sub{fill:var(--comment);font-size:12.5px;}
&lt;/style&gt;
&lt;defs&gt;
&lt;marker id=&quot;ah-g&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;8&quot; markerHeight=&quot;8&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#8A8F98&quot;/&gt;&lt;/marker&gt;
&lt;/defs&gt;

&lt;!-- ghost global observer: a crossed-out frame around all three --&gt;
&lt;rect class=&quot;ghost&quot; x=&quot;24&quot; y=&quot;60&quot; width=&quot;872&quot; height=&quot;300&quot; rx=&quot;10&quot;/&gt;
&lt;line class=&quot;ghostx&quot; x1=&quot;24&quot; y1=&quot;60&quot; x2=&quot;896&quot; y2=&quot;360&quot;/&gt;
&lt;line class=&quot;ghostx&quot; x1=&quot;896&quot; y1=&quot;60&quot; x2=&quot;24&quot; y2=&quot;360&quot;/&gt;
&lt;text class=&quot;ghostlbl&quot; x=&quot;460&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot;&gt;the whole-system observer that cannot exist &amp;mdash; no participant sits here&lt;/text&gt;

&lt;!-- per-node vision boxes (drawn behind nodes) --&gt;
&lt;rect class=&quot;vision&quot; x=&quot;54&quot;  y=&quot;150&quot; width=&quot;300&quot; height=&quot;112&quot; rx=&quot;8&quot;/&gt;
&lt;text class=&quot;visionlbl&quot; x=&quot;60&quot; y=&quot;168&quot;&gt;Root sees: only A&apos;s reply&lt;/text&gt;
&lt;rect class=&quot;vision&quot; x=&quot;374&quot; y=&quot;150&quot; width=&quot;300&quot; height=&quot;112&quot; rx=&quot;8&quot;/&gt;
&lt;text class=&quot;visionlbl&quot; x=&quot;380&quot; y=&quot;168&quot;&gt;A sees: only B&apos;s 200&lt;/text&gt;
&lt;rect class=&quot;vision&quot; x=&quot;694&quot; y=&quot;150&quot; width=&quot;196&quot; height=&quot;112&quot; rx=&quot;8&quot;/&gt;
&lt;text class=&quot;visionlbl&quot; x=&quot;700&quot; y=&quot;168&quot;&gt;B sees: only its own job&lt;/text&gt;

&lt;!-- nodes --&gt;
&lt;rect class=&quot;box&quot; x=&quot;80&quot;  y=&quot;188&quot; width=&quot;150&quot; height=&quot;60&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;155&quot; y=&quot;224&quot; text-anchor=&quot;middle&quot;&gt;Root&lt;/text&gt;
&lt;rect class=&quot;box&quot; x=&quot;400&quot; y=&quot;188&quot; width=&quot;150&quot; height=&quot;60&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;475&quot; y=&quot;224&quot; text-anchor=&quot;middle&quot;&gt;A&lt;/text&gt;
&lt;rect class=&quot;box&quot; x=&quot;720&quot; y=&quot;188&quot; width=&quot;150&quot; height=&quot;60&quot; rx=&quot;6&quot;/&gt;
&lt;text class=&quot;nodelbl&quot; x=&quot;795&quot; y=&quot;224&quot; text-anchor=&quot;middle&quot;&gt;B&lt;/text&gt;

&lt;!-- request + reply edges --&gt;
&lt;line class=&quot;req&quot;   x1=&quot;230&quot; y1=&quot;210&quot; x2=&quot;396&quot; y2=&quot;210&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;line class=&quot;reply&quot; x1=&quot;396&quot; y1=&quot;234&quot; x2=&quot;234&quot; y2=&quot;234&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;replylbl&quot; x=&quot;313&quot; y=&quot;252&quot; text-anchor=&quot;middle&quot;&gt;&quot;done&quot;&lt;/text&gt;
&lt;line class=&quot;req&quot;   x1=&quot;550&quot; y1=&quot;210&quot; x2=&quot;716&quot; y2=&quot;210&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;line class=&quot;reply&quot; x1=&quot;716&quot; y1=&quot;234&quot; x2=&quot;554&quot; y2=&quot;234&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;replylbl&quot; x=&quot;633&quot; y=&quot;252&quot; text-anchor=&quot;middle&quot;&gt;&quot;accepted&quot;&lt;/text&gt;

&lt;!-- B&apos;s crash: visible to B, with no edge back to anyone --&gt;
&lt;line class=&quot;xmark&quot; x1=&quot;786&quot; y1=&quot;300&quot; x2=&quot;804&quot; y2=&quot;318&quot;/&gt;
&lt;line class=&quot;xmark&quot; x1=&quot;804&quot; y1=&quot;300&quot; x2=&quot;786&quot; y2=&quot;318&quot;/&gt;
&lt;text class=&quot;crash&quot; x=&quot;795&quot; y=&quot;344&quot; text-anchor=&quot;middle&quot;&gt;work crashed&lt;/text&gt;
&lt;text class=&quot;crash&quot; x=&quot;795&quot; y=&quot;362&quot; text-anchor=&quot;middle&quot;&gt;&amp;mdash; no one to tell&lt;/text&gt;

&lt;!-- bottom rail: the asymmetry restated --&gt;
&lt;text class=&quot;sub&quot; x=&quot;460&quot; y=&quot;412&quot; text-anchor=&quot;middle&quot;&gt;Every blue box holds a TRUE fact. No box holds the union.&lt;/text&gt;
&lt;text class=&quot;sub&quot; x=&quot;460&quot; y=&quot;434&quot; text-anchor=&quot;middle&quot;&gt;&quot;Is the whole cascade done?&quot; has no box to live in.&lt;/text&gt;
&lt;/svg&gt;&lt;/div&gt; &lt;figcaption data-astro-cid-bj3fsypb&gt;Each node&apos;s vision (blue) covers only its immediate neighbor. The crash in B is real and visible — to B alone, which has no one to tell. The whole-system observer (gray, crossed out) is not missing by accident; in a cross-network mesh, no participant can occupy that vantage point.&lt;/figcaption&gt;  &lt;/figure&gt; 
&lt;p&gt;This is the part that separates a mesh from a single box. On one machine, in one process, you can at least &lt;em&gt;imagine&lt;/em&gt; one watcher seeing everything — the same memory, one event loop you could instrument. (That single-box version has its own quiet failure mode, covered in &lt;a href=&quot;https://thepragmaticquant.com/green-is-not-evidence/&quot;&gt;green is not evidence&lt;/a&gt; — but it lives inside one process where, in principle, one observer could exist.) The moment you cross a network boundary, that imagined watcher is gone for good. A and B are separate processes. The instant A’s request to B closes, the only wire between them is severed. &lt;strong&gt;There is no global observer, and there cannot be one.&lt;/strong&gt; This isn’t a missing feature someone forgot to build; no single participant sits where it could see the whole picture. That is what “distributed” means.&lt;/p&gt;
&lt;h2 id=&quot;just-wait-a-few-seconds-is-not-an-answer&quot;&gt;”Just wait a few seconds” is not an answer&lt;a class=&quot;heading-anchor&quot; href=&quot;#just-wait-a-few-seconds-is-not-an-answer&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Faced with this, the reflex is to throw a timeout at it: &lt;em&gt;give the cascade ten seconds, then call it done.&lt;/em&gt; It feels prudent. It is the wrong &lt;em&gt;shape&lt;/em&gt; of answer, not just a weak one.&lt;/p&gt;
&lt;p&gt;A timeout asks &lt;strong&gt;the clock&lt;/strong&gt;. The question you actually have is about &lt;strong&gt;the mesh&lt;/strong&gt;: has every hop, including ones you can’t see, settled? Those are different questions, and the clock’s answer is never the mesh’s answer:&lt;/p&gt;






























&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Your 10-second timeout fires; you assume done&lt;/th&gt;&lt;th&gt;What is really happening&lt;/th&gt;&lt;th&gt;Verdict it gives you&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Cascade settled in 1 second&lt;/td&gt;&lt;td&gt;The work finished long ago&lt;/td&gt;&lt;td&gt;Right answer, wasted 9 seconds&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A hop is &lt;em&gt;still working&lt;/em&gt; at second 11&lt;/td&gt;&lt;td&gt;Live work, declared dead&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Wrong&lt;/strong&gt; — green over running work&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A hop died silently at second 3&lt;/td&gt;&lt;td&gt;The work is already a corpse&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Wrong&lt;/strong&gt; — &lt;code&gt;SUCCESS&lt;/code&gt; over a corpse&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cascade settled in exactly 10 seconds&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;Right by luck&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;A timeout is right only by coincidence. It can be too slow &lt;em&gt;and&lt;/em&gt; too fast in the same system, because the work it is guessing about has no fixed duration. Tune it short and it fails healthy chains; tune it long and it passes dead ones. There is no right number, because a timeout is not a slightly-weak version of the right check — it is a different category. It samples one node’s clock; the property you want is about &lt;em&gt;all&lt;/em&gt; the edges being empty at once. &lt;strong&gt;You cannot measure a global property with a local stopwatch.&lt;/strong&gt;&lt;/p&gt;
&lt;span class=&quot;marginnote&quot; role=&quot;note&quot;&gt;The honest version of a timeout is “I gave up waiting,” which is a fine thing to say — as long as you don’t print it as &lt;code&gt;SUCCESS&lt;/code&gt;.&lt;/span&gt;
&lt;h2 id=&quot;this-problem-has-a-name-and-its-older-than-you-think&quot;&gt;This problem has a name (and it’s older than you think)&lt;a class=&quot;heading-anchor&quot; href=&quot;#this-problem-has-a-name-and-its-older-than-you-think&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This converts a vague unease into a known, heavy-artillery problem with a literature behind it — the kind of thing worth carrying into your next architecture review, or a board meeting.&lt;/p&gt;
&lt;p&gt;What you are looking at is not a quirk of FastAPI or of agents. Proving that a computation spread across many independent participants has &lt;em&gt;globally&lt;/em&gt; stopped — every node idle, and no work still in flight between them — is a named, studied, genuinely hard problem in distributed systems, and computer scientists have wrestled with it since the early 1980s:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Distributed termination detection&lt;/strong&gt; — proving that a spread-out computation has globally stopped: every participant is idle, &lt;em&gt;and&lt;/em&gt; no work is still traveling between them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is hard for exactly the reason the demo shows. Any single node can be idle &lt;em&gt;right now&lt;/em&gt; while a message carrying more work is traveling toward it. To declare the whole system done, you need to know that all nodes are idle &lt;strong&gt;and&lt;/strong&gt; nothing is in transit — a global property no single participant can see alone. (Our two-hop toy shows the idle-misread half; the “in transit” half is what makes the general A→B→C problem, with a real C still being fed, even harder.) There are real algorithms for it — Dijkstra–Scholten, the Dijkstra–Feijen–van Gasteren token ring, and others — and entire textbook chapters, with named algorithms and proven lower bounds on how much coordination it costs. It is &lt;em&gt;hard on purpose&lt;/em&gt; — which is precisely why it tends to get skipped.&lt;/p&gt;
&lt;p&gt;For the reader who wants one glanceable glossary to walk away with:&lt;/p&gt;

























&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Plain phrase&lt;/th&gt;&lt;th&gt;The CS name&lt;/th&gt;&lt;th&gt;Why it’s hard&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;”Is the whole swarm done?”&lt;/td&gt;&lt;td&gt;Distributed termination detection&lt;/td&gt;&lt;td&gt;No node can see the global state&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”I waited 10s, assume done”&lt;/td&gt;&lt;td&gt;Timeout-based liveness&lt;/td&gt;&lt;td&gt;Guesses with a clock; never observes the mesh&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”B said &lt;code&gt;accepted&lt;/code&gt;”&lt;/td&gt;&lt;td&gt;A local liveness fact&lt;/td&gt;&lt;td&gt;True, but nowhere near sufficient&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Stated plainly: &lt;strong&gt;the agent ecosystem is currently blind to it.&lt;/strong&gt; The frameworks now wiring agents into delegating chains — one model calling another, fanning work across services — are rebuilding multi-node distributed systems without the vocabulary the multi-node world spent forty years building. They ship the topology and skip the termination detection. The result is exactly our demo at scale: swarms of A→B→C cascades that print &lt;code&gt;SUCCESS&lt;/code&gt; and exit &lt;code&gt;0&lt;/code&gt; while the frontier is quietly non-empty.&lt;/p&gt;
&lt;p&gt;I think quiescence — knowing a mesh has actually gone quiet — deserves to be a first-class thing you can &lt;em&gt;ask for&lt;/em&gt;, not a wall-clock guess. And the bar for a real answer is exact: a sound quiescence signal must fire &lt;em&gt;only&lt;/em&gt; when &lt;strong&gt;every node is idle and no work is in transit between them&lt;/strong&gt; — it has to observe the whole active frontier of the mesh, not just the edges any one node can see. A closed stream is not that. A clock is not that. Whether you reach it with the classic algorithms (Dijkstra–Scholten, token rings) or something new, that is the invariant a real answer has to satisfy. We haven’t closed the gap yet, but the shape of the missing primitive is strictly defined.&lt;/p&gt;
&lt;h2 id=&quot;the-proof-that-this-is-real-not-theory&quot;&gt;The proof that this is real, not theory&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-proof-that-this-is-real-not-theory&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is not an extrapolation from a toy. This exact blind spot was sitting in &lt;strong&gt;Google’s reference A2A SDK&lt;/strong&gt; — a teardown bug I found, fixed, and &lt;a href=&quot;https://github.com/a2aproject/a2a-python/pull/1105&quot;&gt;landed upstream&lt;/a&gt;. And it is not one vendor’s slip: the A2A protocol itself punts on the hard part, and the official conformance test kit fakes the answer. Three independent admissions, from three layers of the stack, that the ecosystem has solved “the stream closed” and has not solved “the mesh is done.”&lt;/p&gt;
&lt;p&gt;That is &lt;a href=&quot;https://thepragmaticquant.com/task-was-destroyed-but-pending/&quot;&gt;Task was destroyed but it is pending&lt;/a&gt; — the same blindness, this time with primary sources you can click. The fix I submitted there is real, tested, and &lt;strong&gt;local&lt;/strong&gt;: it makes one process’s teardown deterministic. It does &lt;em&gt;not&lt;/em&gt; hand the ecosystem the missing drain primitive. The in-process bug and the cross-network gap in this demo are the same shape at two zoom levels, but solving the first does not solve the second. The problem is systemic and profound; the fix I submitted is local and precise.&lt;/p&gt;
&lt;h2 id=&quot;the-portable-rule&quot;&gt;The portable rule&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-portable-rule&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;






























&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;What read green&lt;/th&gt;&lt;th&gt;What was actually true&lt;/th&gt;&lt;th&gt;The rule it became&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Root printed &lt;code&gt;SUCCESS&lt;/code&gt;; exit &lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;B dropped the work it accepted; no one upstream can ever learn it&lt;/td&gt;&lt;td&gt;A reply means “my neighbor received it,” never “the work finished&amp;quot;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&amp;quot;The request returned &lt;code&gt;200&lt;/code&gt;”&lt;/td&gt;&lt;td&gt;A local fact about one edge&lt;/td&gt;&lt;td&gt;Never let a local fact stand in for the global one it can’t speak to&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”We waited 10 seconds, nothing screamed”&lt;/td&gt;&lt;td&gt;The clock advanced; the mesh’s actual state was never observed&lt;/td&gt;&lt;td&gt;Gate on an observed settlement signal, never on a wall-clock timeout&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Each node reported truthfully&lt;/td&gt;&lt;td&gt;True local facts composed into a false global one&lt;/td&gt;&lt;td&gt;No participant in a mesh can see the whole frontier — so don’t ask one to&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;If a planted, work-dropping node can survive your cascade and your system stays green, your “done” is decoration. &lt;strong&gt;Stop asking one node whether the swarm finished. No node can answer.&lt;/strong&gt; That is the portable rule — and the reason “are we done yet?” is a harder question than the entire agent industry is currently treating it as.&lt;/p&gt;
&lt;p&gt;The in-process priors are next door. &lt;a href=&quot;https://thepragmaticquant.com/coding-agents-cant-hear-each-other/&quot;&gt;Your AI coding agents can’t hear each other&lt;/a&gt; is the same blindness on a single box — peers that cannot tell when a neighbor finishes or fails. &lt;a href=&quot;https://thepragmaticquant.com/green-is-not-evidence/&quot;&gt;Green is not evidence&lt;/a&gt; is the false-green lens this whole cascade wears. This piece is the cross-process, cross-network sequel: the single box can be given a nervous system, but the mesh does not yet have a way to know it has gone quiet.&lt;/p&gt;
&lt;section class=&quot;faq&quot; aria-label=&quot;Frequently asked questions&quot; data-astro-cid-z6gx6xcw&gt; &lt;h2 data-astro-cid-z6gx6xcw&gt;Frequently asked questions&lt;/h2&gt; &lt;div class=&quot;faq-list&quot; data-astro-cid-z6gx6xcw&gt; &lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;What is the demo actually showing?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;Three programs across a network — Root calls A, A calls B. B answers HTTP 200 &amp;#39;accepted&amp;#39; and then drops the work it promised, which dies a moment later with nobody listening. A only ever saw the 200, so it reports &amp;#39;done&amp;#39;, and Root prints SUCCESS and exits 0. The work never happened, and every participant told the truth about what it could see.&lt;/div&gt; &lt;/details&gt;&lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;Why can&amp;#39;t A just check whether B finished?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;Because once A&amp;#39;s HTTP request to B returns, the connection closes and A and B share no channel. B&amp;#39;s work happens on the far side of a boundary A can no longer see across. A can&amp;#39;t learn B&amp;#39;s downstream fate even in principle — the information channel is gone.&lt;/div&gt; &lt;/details&gt;&lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;What is distributed termination detection?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;It is the classic distributed-systems problem of proving that a computation spread across many independent participants has globally stopped: every node idle AND no work still in transit between them. It has been studied since the early 1980s, with named algorithms (Dijkstra–Scholten and others) and proven lower bounds on coordination cost. It is hard precisely because no single participant can see the global state.&lt;/div&gt; &lt;/details&gt;&lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;How is this different from &amp;#39;green is not evidence&amp;#39;?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;That piece is a single-process failure — one program exits 0 over work it never observed, on one machine, where in principle one observer could exist. This piece is a topology failure across a network boundary: three or more separate processes where no participant can see the whole frontier, so no single observer can exist at all. It is the cross-network cousin, not a rerun.&lt;/div&gt; &lt;/details&gt;&lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;Does the author claim to have solved this?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;No. This piece names the problem; it does not claim to fix the mesh. The only concrete fix — covered in the next piece — is local: it makes one process&amp;#39;s teardown deterministic. The cross-network gap stays open. The stance across both pieces is deliberate: name a systemic failure honestly, while standing behind only the localized fix you can actually defend.&lt;/div&gt; &lt;/details&gt; &lt;/div&gt; &lt;script type=&quot;application/ld+json&quot;&gt;{&quot;@context&quot;:&quot;https://schema.org&quot;,&quot;@type&quot;:&quot;FAQPage&quot;,&quot;mainEntity&quot;:[{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;What is the demo actually showing?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;Three programs across a network — Root calls A, A calls B. B answers HTTP 200 &apos;accepted&apos; and then drops the work it promised, which dies a moment later with nobody listening. A only ever saw the 200, so it reports &apos;done&apos;, and Root prints SUCCESS and exits 0. The work never happened, and every participant told the truth about what it could see.&quot;}},{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;Why can&apos;t A just check whether B finished?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;Because once A&apos;s HTTP request to B returns, the connection closes and A and B share no channel. B&apos;s work happens on the far side of a boundary A can no longer see across. A can&apos;t learn B&apos;s downstream fate even in principle — the information channel is gone.&quot;}},{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;What is distributed termination detection?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;It is the classic distributed-systems problem of proving that a computation spread across many independent participants has globally stopped: every node idle AND no work still in transit between them. It has been studied since the early 1980s, with named algorithms (Dijkstra–Scholten and others) and proven lower bounds on coordination cost. It is hard precisely because no single participant can see the global state.&quot;}},{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;How is this different from &apos;green is not evidence&apos;?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;That piece is a single-process failure — one program exits 0 over work it never observed, on one machine, where in principle one observer could exist. This piece is a topology failure across a network boundary: three or more separate processes where no participant can see the whole frontier, so no single observer can exist at all. It is the cross-network cousin, not a rerun.&quot;}},{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;Does the author claim to have solved this?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;No. This piece names the problem; it does not claim to fix the mesh. The only concrete fix — covered in the next piece — is local: it makes one process&apos;s teardown deterministic. The cross-network gap stays open. The stance across both pieces is deliberate: name a systemic failure honestly, while standing behind only the localized fix you can actually defend.&quot;}}]}&lt;/script&gt; &lt;/section&gt; &lt;!-- Collapsed-by-default FAQ (operator ruling 2026-07-15, supersedes the F-40
     mobile-only collapse): each Q/A pair is a &lt;details&gt;, server-rendered OPEN so
     the answers are visible with no JS, in print, and to crawlers (the full text
     is always in the DOM; the FAQPage JSON-LD above is untouched). This script
     collapses every pair on load AT ALL WIDTHS so the questions read as a
     scannable index and an answer expands on click. astro:page-load covers
     view-transition navigations. --&gt; &lt;script type=&quot;module&quot; src=&quot;https://thepragmaticquant.com/vercel/path0/src/components/Faq.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;The proof that this is real, not theory — the bug in Google’s A2A SDK, the protocol punting on it, the official test kit faking it — is &lt;a href=&quot;https://thepragmaticquant.com/task-was-destroyed-but-pending/&quot;&gt;the next piece&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>agents</category><category>distributed-systems</category><category>the problem</category><category>swarm</category></item><item><title>Task was destroyed but it is pending</title><link>https://thepragmaticquant.com/task-was-destroyed-but-pending/</link><guid isPermaLink="true">https://thepragmaticquant.com/task-was-destroyed-but-pending/</guid><description>A teardown deadlock I diagnosed in Google&apos;s A2A reference SDK — Issue #1101, PR #1105 (417 added lines, 312 of them tests, CI green, merged upstream) — turned out to be one symptom of a primitive the whole agent ecosystem is missing: a sound signal that an async mesh has actually settled.</description><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div class=&quot;tldr&quot;&gt;&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A server running Google’s A2A SDK shuts down mid-cascade and prints &lt;code&gt;Task was destroyed but it is pending!&lt;/code&gt;. Most teams scroll past it. In &lt;a href=&quot;https://github.com/a2aproject/a2a-python&quot;&gt;&lt;code&gt;a2aproject/a2a-python&lt;/code&gt;&lt;/a&gt; it was a real teardown deadlock: a background producer coroutine (a function that can pause and resume) left running while the &lt;code&gt;EventQueue&lt;/code&gt; subscriber it fed was torn down underneath it. The fix — &lt;a href=&quot;https://github.com/a2aproject/a2a-python/issues/1101&quot;&gt;Issue #1101&lt;/a&gt;, &lt;a href=&quot;https://github.com/a2aproject/a2a-python/pull/1105&quot;&gt;PR #1105&lt;/a&gt;, 417 added lines across eight files, 312 of them tests, CI green, merged upstream on 2026-07-20 — is a public async &lt;code&gt;aclose()&lt;/code&gt; with four load-bearing moves, one per section below: it closes the queues &lt;code&gt;immediate=True&lt;/code&gt; (drop pending events instead of waiting to flush them), cancels-and-gathers the background tasks, refuses to resurrect a finished task, and releases an internal lock &lt;em&gt;before&lt;/em&gt; it waits, because the very tasks it is shutting down need that same lock to finish. Hold the lock and everything deadlocks. The fix closes one race. The gap it exposes stays open: the agent ecosystem has no sound way to know when an async mesh is actually &lt;em&gt;done&lt;/em&gt;.&lt;/p&gt;&lt;/div&gt;
&lt;p&gt;An A2A server is shutting down. A push-delegation cascade was in flight a moment ago: agent A handed work to B, who handed it to C. Somewhere in that chain, a producer coroutine is still mid-&lt;code&gt;await&lt;/code&gt; (paused, waiting on something that will now never arrive), feeding an &lt;code&gt;EventQueue&lt;/code&gt; whose subscriber is already being torn down. A producer coroutine fills a queue; a subscriber drains it; the queue is the buffer between them. The event loop, the single scheduler that runs every coroutine in an async Python program, closes. Python prints its one line and moves on:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;text&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Task was destroyed but it is pending!&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Whatever that producer was about to deliver is gone, swallowed with the loop. No exception that reaches your code. No failed status anyone reads. Just an in-flight delegation that quietly never happened, and a warning that fired at interpreter exit, disconnected by then from the request that spawned it.&lt;/p&gt;
&lt;p&gt;That line is the cheapest thing in async Python to ignore. It arrives at shutdown, after the interesting logs, looking like janitorial noise. In &lt;a href=&quot;https://github.com/a2aproject/a2a-python&quot;&gt;&lt;code&gt;a2aproject/a2a-python&lt;/code&gt;&lt;/a&gt; it was not noise. It was a deadlock that happened to lose the race to print before the interpreter exited — the producer’s cleanup was hung, the loop tore down around it, and the “pending” warning was the corpse, not the crime. I went and read it.&lt;/p&gt;
&lt;h2 id=&quot;a-warning-is-a-claim-about-plumbing&quot;&gt;A warning is a claim about plumbing&lt;a class=&quot;heading-anchor&quot; href=&quot;#a-warning-is-a-claim-about-plumbing&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Treat &lt;code&gt;Task was destroyed but it is pending!&lt;/code&gt; the way you should treat a green check: as a claim about &lt;em&gt;plumbing&lt;/em&gt;, not about the world. CPython (the standard Python interpreter) prints it when the event loop is finalized and finds a &lt;code&gt;Task&lt;/code&gt; — an asyncio object wrapping a running coroutine — still alive, still parked on an &lt;code&gt;await&lt;/code&gt; it will never resume. The message is the interpreter telling you the truth it noticed on its way out the door: &lt;em&gt;I was about to run more of your code, and then I wasn’t.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Almost always it is benign. A fire-and-forget coroutine nobody was waiting on, cancelled cleanly at shutdown. So the instinct to scroll past it is, statistically, correct. Which is exactly why it is dangerous when it is not noise. In &lt;a href=&quot;https://github.com/a2aproject/a2a-python/issues/1101&quot;&gt;#1101&lt;/a&gt; the producer was the opposite of idle. It was &lt;em&gt;blocked&lt;/em&gt;, on purpose, by the SDK’s own teardown, holding work the subscriber would never read. The only reason you saw a warning instead of a hang is that the interpreter tore the loop down before the deadlock could announce itself.&lt;/p&gt;
&lt;span class=&quot;marginnote&quot; role=&quot;note&quot;&gt;The benign case and the deadlock case print the identical line. That collision is the whole trap: the failure wears the costume of the most-ignorable warning in async Python.&lt;/span&gt;
&lt;h2 id=&quot;where-the-producer-goes-to-die&quot;&gt;Where the producer goes to die&lt;a class=&quot;heading-anchor&quot; href=&quot;#where-the-producer-goes-to-die&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To see why the producer was blocked and not idle, walk the lifecycle the PR exposes. A &lt;code&gt;DefaultRequestHandlerV2&lt;/code&gt; owns an &lt;code&gt;ActiveTaskRegistry&lt;/code&gt;; the registry owns one &lt;code&gt;ActiveTask&lt;/code&gt; per in-flight request. Each &lt;code&gt;ActiveTask&lt;/code&gt; runs a &lt;strong&gt;producer&lt;/strong&gt; — the coroutine generating events for that request — that writes into an &lt;code&gt;EventQueue&lt;/code&gt;, and a &lt;strong&gt;subscriber&lt;/strong&gt; that drains the queue and forwards events out, to the caller or, in a delegating mesh, to the next agent in the cascade. Producer fills, subscriber empties; the queue is the seam between them.&lt;/p&gt;
&lt;p&gt;The reproduction is exact, and it is in the public issue. When a request reaches a terminal state at the application layer — finished, failed, or cancelled, no more events coming — the owning &lt;code&gt;ActiveTask&lt;/code&gt; begins cleaning up its background producer, and that cleanup awaits:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;python&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# the shape of the trap on the public PR surface, not project source&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._event_queue_subscribers.close(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFA657&quot;&gt;immediate&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;close(immediate=False)&lt;/code&gt; means &lt;em&gt;drain gracefully&lt;/em&gt;: wait for every queued event and every child sink to flush. But the subscriber on the other end of that queue is the thing being torn down. Nobody is reading. The drain waits for a consumer that is never coming back, the producer’s cleanup never returns, and the task stays pending until the loop is yanked out from under it at shutdown. The graceful path is the hang.&lt;/p&gt;
&lt;p&gt;A correct teardown has to reach &lt;em&gt;both&lt;/em&gt; ends of the seam — close the reader and reckon with the still-live writer — without leaving the loop holding a coroutine blocked on the very queue you just closed. The pair was started together and was never stopped together. That asymmetry, a spawn with no symmetric awaited teardown, is the bug in one sentence. The fix is four moves; here they are in the order the teardown actually executes them.&lt;/p&gt;
&lt;h2 id=&quot;immediatetrue-or-you-drain-forever&quot;&gt;immediate=True, or you drain forever&lt;a class=&quot;heading-anchor&quot; href=&quot;#immediatetrue-or-you-drain-forever&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first move runs against instinct. The reflex at shutdown is to drain &lt;em&gt;gracefully&lt;/em&gt;: let the queue empty, let the producer finish its last item, then close. You cannot do that here. &lt;strong&gt;You cannot gracefully drain a queue whose producer you are simultaneously cancelling.&lt;/strong&gt; “Drain” means “let the producer finish pushing and the consumer finish reading,” and you are in the middle of cancelling that producer. A graceful drain would wait for events the cancelled producer will never push, which is the original hang wearing a more reassuring name.&lt;/p&gt;
&lt;p&gt;So the queues close &lt;code&gt;immediate=True&lt;/code&gt;: stop accepting new events, and force a producer blocked on the queue to raise out of its &lt;code&gt;await&lt;/code&gt; instead of parking there forever. Close immediately &lt;em&gt;first&lt;/em&gt;. Only then cancel-and-gather the background tasks — that is the next move, and it is where the real bug lives.&lt;/p&gt;
&lt;h2 id=&quot;the-obvious-fix-re-deadlocks&quot;&gt;The obvious fix re-deadlocks&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-obvious-fix-re-deadlocks&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The obvious next step is to stop waiting on a dead consumer: cancel the background tasks, &lt;code&gt;gather&lt;/code&gt; them (wait for all of them together) so their exceptions surface, and be done. And because the registry is shared mutable state being changed during teardown, you hold the registry lock while you do it. That reads as careful.&lt;/p&gt;
&lt;p&gt;It looks correct. It hangs forever.&lt;/p&gt;
&lt;span class=&quot;marginnote&quot; role=&quot;note&quot;&gt;This is the whole article in one bug: the version that re-deadlocks and the version that works differ by where one &lt;code&gt;await&lt;/code&gt; sits relative to a lock. Structured concurrency is mostly a discipline about exactly that.&lt;/span&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;python&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# the trap: cancel-and-gather WHILE holding the registry lock&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#D2A8FF&quot;&gt; aclose&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(self) -&amp;gt; &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; with&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._lock:                     &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# take the lock&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; task &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._active.values():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;            task.cancel()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; asyncio.gather(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._active.values(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFA657&quot;&gt;                             return_exceptions&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# &amp;lt;-- never returns: deadlock&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The reason is the re-entrant detail that separates a real fix from a plausible one. When you cancel an &lt;code&gt;ActiveTask&lt;/code&gt;, its &lt;em&gt;own&lt;/em&gt; teardown path runs, and that teardown reaches back into the registry to deregister itself from the map — which needs the same registry lock you are currently holding. So &lt;code&gt;gather&lt;/code&gt; waits for the tasks to finish, the tasks wait for the lock to deregister, and the lock waits for &lt;code&gt;gather&lt;/code&gt; to return it. Three parties, one cycle, no progress. The cancel you issued to break the deadlock created a tighter one. (Re-entrant here just means the cancelled task reaches back for a lock the canceller already holds.)&lt;/p&gt;
&lt;figure class=&quot;full&quot; data-astro-cid-bj3fsypb&gt; &lt;div class=&quot;svgfig svgfig-diagram&quot; role=&quot;img&quot; aria-label=&quot;A three-node wait-for graph forming a closed cycle. await gather() waits for the cancelled tasks to finish; the cancelled tasks each need the registry lock to deregister; the registry lock is held by the canceller and never released — back to await gather(). All three edges are red, forming an unbroken cycle with no progress.&quot; data-astro-cid-bj3fsypb&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; width=&quot;760&quot; id=&quot;fig-07-deadlock-cycle&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 760 440&quot; role=&quot;graphics-document&quot; style=&quot;background-color: transparent;&quot;&gt;&lt;style&gt;#fig-07-deadlock-cycle [fill=&quot;#1a1d23&quot; i]{fill:var(--bg)}#fig-07-deadlock-cycle [stroke=&quot;#1a1d23&quot; i]{stroke:var(--bg)}#fig-07-deadlock-cycle [fill=&quot;#18181b&quot; i]{fill:var(--bg-panel)}#fig-07-deadlock-cycle [stroke=&quot;#18181b&quot; i]{stroke:var(--bg-panel)}#fig-07-deadlock-cycle [fill=&quot;#2c2f37&quot; i]{fill:var(--current-line)}#fig-07-deadlock-cycle [stroke=&quot;#2c2f37&quot; i]{stroke:var(--current-line)}#fig-07-deadlock-cycle [fill=&quot;#f4f5f7&quot; i]{fill:var(--fg)}#fig-07-deadlock-cycle [stroke=&quot;#f4f5f7&quot; i]{stroke:var(--fg)}#fig-07-deadlock-cycle [fill=&quot;#8a8f98&quot; i]{fill:var(--comment)}#fig-07-deadlock-cycle [stroke=&quot;#8a8f98&quot; i]{stroke:var(--comment)}&lt;/style&gt;
&lt;style&gt;
#fig-07-deadlock-cycle{font-family:var(--mono);}
#fig-07-deadlock-cycle .node{fill:var(--bg-panel);stroke:var(--comment);stroke-width:1.5px;}
#fig-07-deadlock-cycle .nlabel{fill:var(--fg);font-size:19px;font-weight:600;}
#fig-07-deadlock-cycle .nsub{fill:var(--comment);font-size:13px;}
#fig-07-deadlock-cycle .cyc{stroke:#D55E00;stroke-width:3px;fill:none;}
#fig-07-deadlock-cycle .clabel{fill:#D55E00;font-size:14px;font-weight:600;}
#fig-07-deadlock-cycle .cbg{fill:var(--bg);}
&lt;/style&gt;
&lt;defs&gt;
&lt;marker id=&quot;ah-red&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;
&lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#D55E00&quot;/&gt;
&lt;/marker&gt;
&lt;/defs&gt;

&lt;!-- nodes: Gather (top), Registry lock (bottom-left), Tasks (bottom-right) --&gt;
&lt;g&gt;
&lt;rect class=&quot;node&quot; x=&quot;288&quot; y=&quot;40&quot; rx=&quot;8&quot; width=&quot;184&quot; height=&quot;68&quot;/&gt;
&lt;text class=&quot;nlabel&quot; x=&quot;380&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot;&gt;await gather(...)&lt;/text&gt;
&lt;text class=&quot;nsub&quot; x=&quot;380&quot; y=&quot;92&quot; text-anchor=&quot;middle&quot;&gt;the canceller, blocked&lt;/text&gt;
&lt;/g&gt;
&lt;g&gt;
&lt;rect class=&quot;node&quot; x=&quot;60&quot; y=&quot;320&quot; rx=&quot;8&quot; width=&quot;200&quot; height=&quot;68&quot;/&gt;
&lt;text class=&quot;nlabel&quot; x=&quot;160&quot; y=&quot;350&quot; text-anchor=&quot;middle&quot;&gt;registry lock&lt;/text&gt;
&lt;text class=&quot;nsub&quot; x=&quot;160&quot; y=&quot;372&quot; text-anchor=&quot;middle&quot;&gt;held by the canceller&lt;/text&gt;
&lt;/g&gt;
&lt;g&gt;
&lt;rect class=&quot;node&quot; x=&quot;500&quot; y=&quot;320&quot; rx=&quot;8&quot; width=&quot;200&quot; height=&quot;68&quot;/&gt;
&lt;text class=&quot;nlabel&quot; x=&quot;600&quot; y=&quot;350&quot; text-anchor=&quot;middle&quot;&gt;cancelled tasks&lt;/text&gt;
&lt;text class=&quot;nsub&quot; x=&quot;600&quot; y=&quot;372&quot; text-anchor=&quot;middle&quot;&gt;tearing themselves down&lt;/text&gt;
&lt;/g&gt;

&lt;!-- cycle edges (all red, directed): gather -&gt; tasks -&gt; lock -&gt; gather --&gt;
&lt;!-- gather -&gt; tasks --&gt;
&lt;path class=&quot;cyc&quot; d=&quot;M 458 104 Q 560 200 600 316&quot; marker-end=&quot;url(#ah-red)&quot;/&gt;
&lt;g&gt;&lt;rect class=&quot;cbg&quot; x=&quot;540&quot; y=&quot;188&quot; width=&quot;168&quot; height=&quot;24&quot;/&gt;&lt;text class=&quot;clabel&quot; x=&quot;548&quot; y=&quot;205&quot;&gt;waits for them to finish&lt;/text&gt;&lt;/g&gt;

&lt;!-- tasks -&gt; lock --&gt;
&lt;path class=&quot;cyc&quot; d=&quot;M 496 358 L 264 358&quot; marker-end=&quot;url(#ah-red)&quot;/&gt;
&lt;g&gt;&lt;rect class=&quot;cbg&quot; x=&quot;300&quot; y=&quot;380&quot; width=&quot;160&quot; height=&quot;44&quot;/&gt;&lt;text class=&quot;clabel&quot; x=&quot;380&quot; y=&quot;397&quot; text-anchor=&quot;middle&quot;&gt;each needs the lock&lt;/text&gt;&lt;text class=&quot;clabel&quot; x=&quot;380&quot; y=&quot;415&quot; text-anchor=&quot;middle&quot;&gt;to deregister&lt;/text&gt;&lt;/g&gt;

&lt;!-- lock -&gt; gather --&gt;
&lt;path class=&quot;cyc&quot; d=&quot;M 160 316 Q 120 200 302 104&quot; marker-end=&quot;url(#ah-red)&quot;/&gt;
&lt;g&gt;&lt;rect class=&quot;cbg&quot; x=&quot;44&quot; y=&quot;188&quot; width=&quot;170&quot; height=&quot;24&quot;/&gt;&lt;text class=&quot;clabel&quot; x=&quot;52&quot; y=&quot;205&quot;&gt;never released&lt;/text&gt;&lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt; &lt;figcaption data-astro-cid-bj3fsypb&gt;The cancel-and-gather deadlock, as a wait-for graph. Each red edge is a party waiting on the next; the cycle closes with no one able to advance. &lt;code&gt;gather&lt;/code&gt; holds the lock and waits for the tasks; the tasks need that same lock to deregister. Hold the lock across the &lt;code&gt;await&lt;/code&gt; and the loop is sealed.&lt;/figcaption&gt;  &lt;/figure&gt; &lt;script type=&quot;module&quot; src=&quot;https://thepragmaticquant.com/vercel/path0/src/components/Figure.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;The &lt;code&gt;X is not Y&lt;/code&gt; here: &lt;strong&gt;holding the lock is not owning the teardown.&lt;/strong&gt; The lock protects the &lt;em&gt;membership set&lt;/em&gt; — the act of mutating the map. It was never a license to block inside it while the things you are tearing down need it to finish. The fix shrinks the lock to exactly what it protects and moves the &lt;code&gt;await&lt;/code&gt; outside:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;python&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# the fix: snapshot under the lock, await OUTSIDE it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#D2A8FF&quot;&gt; aclose&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(self) -&amp;gt; &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; with&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._lock:                     &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# lock protects the snapshot only&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._is_finished &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; True&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;        tasks &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; list&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._active.values())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;    # lock released here — the tasks&amp;#39; teardown can now re-acquire it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; task &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; tasks:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;        task.cancel()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt; asyncio.gather(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;tasks, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFA657&quot;&gt;return_exceptions&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Take the lock only long enough to read a consistent snapshot and mark the registry closed. Release it. &lt;em&gt;Then&lt;/em&gt; cancel and gather, so each cancelled task can re-enter, deregister, and complete, and the gather returns. That single move — snapshot inside, await outside — is the difference between a fix that works and one that re-deadlocks while passing review. You only find it by reading what the cancelled tasks do on their way out, not by reasoning about the canceller alone.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;gather&lt;/code&gt; with &lt;code&gt;return_exceptions=True&lt;/code&gt; is doing more than waiting. It means the &lt;code&gt;CancelledError&lt;/code&gt; each cancelled coroutine raises, plus any real exception, &lt;em&gt;surfaces&lt;/em&gt; into the gathered result instead of evaporating as an exit-time “pending” warning. A bare &lt;code&gt;cancel()&lt;/code&gt; without the gather drops the tasks’ exceptions on the floor and reintroduces the exact ghost. The gather is how you convert a silent loop-finalization into a value you can inspect — the difference between a teardown and a &lt;code&gt;del&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;no-resurrection&quot;&gt;No resurrection&lt;a class=&quot;heading-anchor&quot; href=&quot;#no-resurrection&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Releasing the lock before awaiting is correct, but it forces a second invariant to carry weight, and &lt;code&gt;aclose()&lt;/code&gt; is incomplete without it. While &lt;code&gt;aclose()&lt;/code&gt; is mid-gather with the lock released, a late &lt;code&gt;start()&lt;/code&gt; can arrive — a request that was already in flight when shutdown began — and try to spawn a &lt;em&gt;fresh&lt;/em&gt; producer into the registry &lt;code&gt;aclose()&lt;/code&gt; thinks it just emptied. Leave that path open and you finish shutting down with a new background coroutine running past the barrier you closed, feeding a queue nobody will ever drain.&lt;/p&gt;
&lt;p&gt;The flag and the lock-discipline are &lt;em&gt;one&lt;/em&gt; invariant, not two fixes. &lt;code&gt;aclose()&lt;/code&gt; sets &lt;code&gt;_is_finished&lt;/code&gt; &lt;strong&gt;in the same critical section&lt;/strong&gt; as the snapshot — atomic close-marking — and &lt;code&gt;start()&lt;/code&gt; checks that flag &lt;strong&gt;under the same lock&lt;/strong&gt; before it spawns:&lt;/p&gt;
&lt;div class=&quot;codeblock&quot;&gt;&lt;div class=&quot;codeblock-head&quot;&gt;&lt;span class=&quot;lang&quot;&gt;python&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;copy-btn&quot; data-copy aria-label=&quot;Copy code&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;rect width=&quot;14&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;8&quot; rx=&quot;2&quot; ry=&quot;2&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;span class=&quot;copy-label&quot;&gt;Copy&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-default&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e6edf3;--shiki-light-bg:#fff;--shiki-dark-bg:#0d1117;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#8B949E&quot;&gt;# finished is terminal: refuse to start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#D2A8FF&quot;&gt; start&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(self) -&amp;gt; &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt; with&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._lock:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._is_finished:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#FF7B72&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt; RuntimeError&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#A5D6FF&quot;&gt;&amp;quot;task is finished; refusing to start&amp;quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79C0FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E6EDF3&quot;&gt;._spawn_producer()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Because &lt;code&gt;_is_finished&lt;/code&gt; is set inside the lock and read inside the lock, a &lt;code&gt;start()&lt;/code&gt; that races the gather sees the closed flag and refuses, every time. There is no window. Name it as a rule the state machine enforces: &lt;strong&gt;a &lt;code&gt;finished&lt;/code&gt; state that a late &lt;code&gt;start()&lt;/code&gt; can quietly undo is just a suggestion.&lt;/strong&gt; The lifecycle gets a head, a tail, and a one-way door between them.&lt;/p&gt;
&lt;p&gt;That is the whole fix: a public async &lt;code&gt;aclose()&lt;/code&gt; on &lt;code&gt;DefaultRequestHandlerV2&lt;/code&gt;, &lt;code&gt;ActiveTaskRegistry&lt;/code&gt;, and &lt;code&gt;ActiveTask&lt;/code&gt; that closes queues &lt;code&gt;immediate=True&lt;/code&gt;, releases the lock before awaiting, cancels-then-gathers, and refuses resurrection. &lt;a href=&quot;https://github.com/a2aproject/a2a-python/pull/1105&quot;&gt;PR #1105&lt;/a&gt; adds 417 lines across eight files and deletes none — 312 of those lines are tests that reproduce the deadlock deterministically — and it merged into the reference SDK on 2026-07-20 with CI green. The load-bearing hunk is the lock-scope change in &lt;code&gt;aclose()&lt;/code&gt; — open the PR and the snapshot-under-lock, gather-outside-lock shape is right there.&lt;/p&gt;
&lt;h2 id=&quot;zoom-out-this-was-a-symptom&quot;&gt;Zoom out: this was a symptom&lt;a class=&quot;heading-anchor&quot; href=&quot;#zoom-out-this-was-a-symptom&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now the part that outlasts the bug.&lt;/p&gt;
&lt;p&gt;This race isn’t a one-off process quirk; it is a structural class of failure. Step back from the single Python loop and look at the cascade: there is no local way to know when an A→B→C push-delegating chain has &lt;em&gt;actually settled&lt;/em&gt;. A closed its stream, but a closed stream on A says nothing about whether B’s push to C is still in flight.&lt;/p&gt;
&lt;p&gt;The teardown race is precisely what the gap between a local fact and a global one looks like inside a single process.&lt;/p&gt;
&lt;p&gt;The handler believed the request was done — terminal status reached — while a background producer was still mid-&lt;code&gt;await&lt;/code&gt;. “This stream closed” is a &lt;em&gt;local&lt;/em&gt; fact. “The cascade has settled” is a &lt;em&gt;global&lt;/em&gt; one, and nothing in the loop was positioned to tell the difference. Multiply that across processes and a network and you get the same disagreement at the scale of a mesh: A reports done while B is still pushing to C, and no participant is positioned to notice.&lt;/p&gt;
&lt;p&gt;The protocol made this harder on purpose. The A2A v1.0 spec &lt;strong&gt;removed the per-task &lt;code&gt;final&lt;/code&gt; flag&lt;/strong&gt;. Earlier drafts let a task-status event carry an explicit &lt;code&gt;final: true&lt;/code&gt; — a producer-asserted “this stream is done” marker. v1.0 dropped it; &lt;a href=&quot;https://a2a-protocol.org/latest/whats-new-v1/&quot;&gt;the changelog says to “leverage protocol binding specific stream closure mechanism instead”&lt;/a&gt;. Completion is now &lt;em&gt;inferred&lt;/em&gt; from stream-closure or terminal status, never &lt;em&gt;asserted&lt;/em&gt;. That is a defensible protocol simplification, and it quietly relocated a hard problem from the wire onto every implementer’s teardown path.&lt;/p&gt;
&lt;p&gt;Go looking for the primitive that would close the gap, and it is not there. &lt;strong&gt;No A2A SDK — Python, JS, or Go — exposes a &lt;code&gt;wait_for_idle&lt;/code&gt;, a &lt;code&gt;drain&lt;/code&gt;, or any “all background work is done” signal.&lt;/strong&gt; The thing every distributed-systems textbook calls &lt;em&gt;termination detection&lt;/em&gt;, proving a distributed computation has globally stopped, has no entry in the agent-protocol vocabulary.&lt;/p&gt;
&lt;span class=&quot;marginnote&quot; role=&quot;note&quot;&gt;I checked the Python, JS, and Go SDKs and the conformance kit. If a drain primitive exists in one I missed, that is the bug report I want — the claim is falsifiable on purpose.&lt;/span&gt;
&lt;h2 id=&quot;fifteen-seconds-is-not-a-proof&quot;&gt;Fifteen seconds is not a proof&lt;a class=&quot;heading-anchor&quot; href=&quot;#fifteen-seconds-is-not-a-proof&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The conformance kit gives the gap away. The canonical A2A conformance kit, &lt;a href=&quot;https://github.com/a2aproject/a2a-tck&quot;&gt;&lt;code&gt;a2a-tck&lt;/code&gt;&lt;/a&gt;, tests push-notification delivery — and to decide whether a webhook arrived, it falls back to a hardcoded fifteen-second wait. Not a drain signal. Not a quiescence check. A wall-clock guess, because there is no quiescent signal to wait on.&lt;/p&gt;
&lt;figure class=&quot;full&quot; data-astro-cid-bj3fsypb&gt; &lt;div class=&quot;svgfig svgfig-diagram&quot; role=&quot;img&quot; aria-label=&quot;Two side-by-side space-time diagrams with time flowing downward and lifelines Root, A, B, C. Left (&apos;timeout&apos;): a push cascade Root to A to B to C; a red dashed horizontal 15-second timeout line cuts across the B-to-C arrow before it lands, marked with a red X — the mesh exits green while a hop is still in flight. Right (&apos;drain&apos;): the same cascade, but every hop&apos;s acknowledgement flows back up to Root, and a green dashed drain line sits below the final return — settlement is only declared once the frontier is empty.&quot; data-astro-cid-bj3fsypb&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; width=&quot;920&quot; id=&quot;fig-07-timeout-vs-drain&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 920 555&quot; role=&quot;graphics-document&quot; style=&quot;background-color: transparent;&quot;&gt;&lt;style&gt;#fig-07-timeout-vs-drain [fill=&quot;#1a1d23&quot; i]{fill:var(--bg)}#fig-07-timeout-vs-drain [stroke=&quot;#1a1d23&quot; i]{stroke:var(--bg)}#fig-07-timeout-vs-drain [fill=&quot;#18181b&quot; i]{fill:var(--bg-panel)}#fig-07-timeout-vs-drain [stroke=&quot;#18181b&quot; i]{stroke:var(--bg-panel)}#fig-07-timeout-vs-drain [fill=&quot;#2c2f37&quot; i]{fill:var(--current-line)}#fig-07-timeout-vs-drain [stroke=&quot;#2c2f37&quot; i]{stroke:var(--current-line)}#fig-07-timeout-vs-drain [fill=&quot;#f4f5f7&quot; i]{fill:var(--fg)}#fig-07-timeout-vs-drain [stroke=&quot;#f4f5f7&quot; i]{stroke:var(--fg)}#fig-07-timeout-vs-drain [fill=&quot;#8a8f98&quot; i]{fill:var(--comment)}#fig-07-timeout-vs-drain [stroke=&quot;#8a8f98&quot; i]{stroke:var(--comment)}&lt;/style&gt;
&lt;style&gt;
#fig-07-timeout-vs-drain{font-family:var(--mono);}
#fig-07-timeout-vs-drain .ll{stroke:var(--current-line);stroke-width:2px;stroke-dasharray:4 5;}
#fig-07-timeout-vs-drain .lh{fill:var(--fg);font-size:19px;font-weight:600;}
#fig-07-timeout-vs-drain .ptitle{fill:var(--comment);font-size:15px;font-weight:600;letter-spacing:.4px;}
#fig-07-timeout-vs-drain .msg{stroke:var(--comment);stroke-width:2.5px;fill:none;}
#fig-07-timeout-vs-drain .ret{stroke:var(--comment);stroke-width:2px;stroke-dasharray:5 4;fill:none;}
#fig-07-timeout-vs-drain .mlabel{fill:var(--comment);font-size:15px;}
#fig-07-timeout-vs-drain .timeout{stroke:#D55E00;stroke-width:3.5px;stroke-dasharray:9 5;}
#fig-07-timeout-vs-drain .drain{stroke:#009E73;stroke-width:3.5px;stroke-dasharray:9 5;}
#fig-07-timeout-vs-drain .tlabel{fill:#D55E00;font-size:16.5px;font-weight:600;}
#fig-07-timeout-vs-drain .dlabel{fill:#009E73;font-size:16.5px;font-weight:600;}
#fig-07-timeout-vs-drain .xmark{stroke:#D55E00;stroke-width:4.5px;}
#fig-07-timeout-vs-drain .divider{stroke:var(--current-line);stroke-width:1.5px;}
#fig-07-timeout-vs-drain .axis{fill:var(--comment);font-size:14px;font-style:italic;}
&lt;/style&gt;
&lt;defs&gt;
&lt;marker id=&quot;ah-g&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;8&quot; markerHeight=&quot;8&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#8A8F98&quot;/&gt;&lt;/marker&gt;
&lt;/defs&gt;

&lt;text class=&quot;axis&quot; x=&quot;16&quot; y=&quot;110&quot; transform=&quot;rotate(90 16 110)&quot;&gt;time →&lt;/text&gt;
&lt;line class=&quot;divider&quot; x1=&quot;460&quot; y1=&quot;24&quot; x2=&quot;460&quot; y2=&quot;525&quot;/&gt;

&lt;!-- ================= LEFT PANEL: timeout / false green ================= --&gt;
&lt;text class=&quot;ptitle&quot; x=&quot;44&quot; y=&quot;40&quot;&gt;TIMEOUT — exits green, mesh not settled&lt;/text&gt;
&lt;g&gt;
&lt;text class=&quot;lh&quot; x=&quot;55&quot;  y=&quot;74&quot; text-anchor=&quot;middle&quot;&gt;Root&lt;/text&gt;
&lt;text class=&quot;lh&quot; x=&quot;175&quot; y=&quot;74&quot; text-anchor=&quot;middle&quot;&gt;A&lt;/text&gt;
&lt;text class=&quot;lh&quot; x=&quot;300&quot; y=&quot;74&quot; text-anchor=&quot;middle&quot;&gt;B&lt;/text&gt;
&lt;text class=&quot;lh&quot; x=&quot;420&quot; y=&quot;74&quot; text-anchor=&quot;middle&quot;&gt;C&lt;/text&gt;
&lt;line class=&quot;ll&quot; x1=&quot;55&quot;  y1=&quot;84&quot; x2=&quot;55&quot;  y2=&quot;520&quot;/&gt;
&lt;line class=&quot;ll&quot; x1=&quot;175&quot; y1=&quot;84&quot; x2=&quot;175&quot; y2=&quot;520&quot;/&gt;
&lt;line class=&quot;ll&quot; x1=&quot;300&quot; y1=&quot;84&quot; x2=&quot;300&quot; y2=&quot;520&quot;/&gt;
&lt;line class=&quot;ll&quot; x1=&quot;420&quot; y1=&quot;84&quot; x2=&quot;420&quot; y2=&quot;520&quot;/&gt;
&lt;/g&gt;
&lt;line class=&quot;msg&quot; x1=&quot;55&quot;  y1=&quot;130&quot; x2=&quot;175&quot; y2=&quot;130&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;mlabel&quot; x=&quot;115&quot; y=&quot;122&quot; text-anchor=&quot;middle&quot;&gt;delegate&lt;/text&gt;
&lt;line class=&quot;msg&quot; x1=&quot;175&quot; y1=&quot;190&quot; x2=&quot;300&quot; y2=&quot;190&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;mlabel&quot; x=&quot;237&quot; y=&quot;182&quot; text-anchor=&quot;middle&quot;&gt;push&lt;/text&gt;
&lt;line class=&quot;msg&quot; x1=&quot;300&quot; y1=&quot;255&quot; x2=&quot;420&quot; y2=&quot;330&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;mlabel&quot; x=&quot;318&quot; y=&quot;250&quot;&gt;push (slow)&lt;/text&gt;
&lt;line class=&quot;timeout&quot; x1=&quot;28&quot; y1=&quot;290&quot; x2=&quot;448&quot; y2=&quot;290&quot;/&gt;
&lt;text class=&quot;tlabel&quot; x=&quot;28&quot; y=&quot;282&quot;&gt;15s timeout fires&lt;/text&gt;
&lt;line class=&quot;xmark&quot; x1=&quot;347&quot; y1=&quot;281&quot; x2=&quot;365&quot; y2=&quot;299&quot;/&gt;
&lt;line class=&quot;xmark&quot; x1=&quot;365&quot; y1=&quot;281&quot; x2=&quot;347&quot; y2=&quot;299&quot;/&gt;
&lt;text class=&quot;tlabel&quot; x=&quot;245&quot; y=&quot;372&quot; text-anchor=&quot;middle&quot;&gt;B&amp;#8594;C still in flight when&lt;/text&gt;
&lt;text class=&quot;tlabel&quot; x=&quot;245&quot; y=&quot;394&quot; text-anchor=&quot;middle&quot;&gt;the clock says &quot;done&quot;. Exit 0.&lt;/text&gt;

&lt;!-- ================= RIGHT PANEL: drain / true quiescence ================= --&gt;
&lt;text class=&quot;ptitle&quot; x=&quot;478&quot; y=&quot;40&quot;&gt;DRAIN — settles when the frontier empties&lt;/text&gt;
&lt;g&gt;
&lt;text class=&quot;lh&quot; x=&quot;510&quot; y=&quot;74&quot; text-anchor=&quot;middle&quot;&gt;Root&lt;/text&gt;
&lt;text class=&quot;lh&quot; x=&quot;630&quot; y=&quot;74&quot; text-anchor=&quot;middle&quot;&gt;A&lt;/text&gt;
&lt;text class=&quot;lh&quot; x=&quot;750&quot; y=&quot;74&quot; text-anchor=&quot;middle&quot;&gt;B&lt;/text&gt;
&lt;text class=&quot;lh&quot; x=&quot;875&quot; y=&quot;74&quot; text-anchor=&quot;middle&quot;&gt;C&lt;/text&gt;
&lt;line class=&quot;ll&quot; x1=&quot;510&quot; y1=&quot;84&quot; x2=&quot;510&quot; y2=&quot;520&quot;/&gt;
&lt;line class=&quot;ll&quot; x1=&quot;630&quot; y1=&quot;84&quot; x2=&quot;630&quot; y2=&quot;520&quot;/&gt;
&lt;line class=&quot;ll&quot; x1=&quot;750&quot; y1=&quot;84&quot; x2=&quot;750&quot; y2=&quot;520&quot;/&gt;
&lt;line class=&quot;ll&quot; x1=&quot;875&quot; y1=&quot;84&quot; x2=&quot;875&quot; y2=&quot;520&quot;/&gt;
&lt;/g&gt;
&lt;line class=&quot;msg&quot; x1=&quot;510&quot; y1=&quot;130&quot; x2=&quot;630&quot; y2=&quot;130&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;mlabel&quot; x=&quot;570&quot; y=&quot;122&quot; text-anchor=&quot;middle&quot;&gt;delegate&lt;/text&gt;
&lt;line class=&quot;msg&quot; x1=&quot;630&quot; y1=&quot;185&quot; x2=&quot;750&quot; y2=&quot;185&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;line class=&quot;msg&quot; x1=&quot;750&quot; y1=&quot;240&quot; x2=&quot;875&quot; y2=&quot;258&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;line class=&quot;ret&quot; x1=&quot;875&quot; y1=&quot;310&quot; x2=&quot;750&quot; y2=&quot;328&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;line class=&quot;ret&quot; x1=&quot;750&quot; y1=&quot;365&quot; x2=&quot;630&quot; y2=&quot;383&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;line class=&quot;ret&quot; x1=&quot;630&quot; y1=&quot;420&quot; x2=&quot;510&quot; y2=&quot;438&quot; marker-end=&quot;url(#ah-g)&quot;/&gt;
&lt;text class=&quot;mlabel&quot; x=&quot;800&quot; y=&quot;305&quot;&gt;done&lt;/text&gt;
&lt;text class=&quot;mlabel&quot; x=&quot;540&quot; y=&quot;415&quot;&gt;last ack&lt;/text&gt;
&lt;line class=&quot;drain&quot; x1=&quot;488&quot; y1=&quot;478&quot; x2=&quot;900&quot; y2=&quot;478&quot;/&gt;
&lt;text class=&quot;dlabel&quot; x=&quot;488&quot; y=&quot;470&quot;&gt;frontier empty&lt;/text&gt;
&lt;text class=&quot;dlabel&quot; x=&quot;695&quot; y=&quot;520&quot; text-anchor=&quot;middle&quot;&gt;every hop acked, then — and only then —&lt;/text&gt;
&lt;text class=&quot;dlabel&quot; x=&quot;695&quot; y=&quot;542&quot; text-anchor=&quot;middle&quot;&gt;the drain signal fires. Safe to tear down.&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt; &lt;figcaption data-astro-cid-bj3fsypb&gt;Same race, two ways to call it done. &lt;strong&gt;Left:&lt;/strong&gt; the 15-second timeout fires while B→C is still in flight (the red X) — exit-0 over an undrained hop. &lt;strong&gt;Right:&lt;/strong&gt; the drain signal fires only after the last acknowledgement returns and the frontier is empty. A timeout asks the clock; a drain asks the mesh.&lt;/figcaption&gt;  &lt;/figure&gt; 
&lt;p&gt;When the &lt;em&gt;reference conformance suite&lt;/em&gt; — the thing whose whole job is to define “correct” — resorts to “wait fifteen seconds and assume,” that is not a tooling shortcut. It is the ecosystem admitting it has no sound primitive to test against. This is the &lt;a href=&quot;https://thepragmaticquant.com/green-is-not-evidence/&quot;&gt;green-is-not-evidence&lt;/a&gt; failure class natively ported to agents: a shutdown that exits &lt;code&gt;0&lt;/code&gt; over a leaked, undrained producer reads &lt;em&gt;identically&lt;/em&gt; to a clean one. Exit-0 guarantees the loop returned; it proves nothing about delegations draining. A fifteen-second timeout just means the clock advanced, not that the mesh settled.&lt;/p&gt;
&lt;p&gt;And a timeout is not a slightly-weak quiescence check — it is the wrong &lt;em&gt;shape&lt;/em&gt; of check. A push-delegating A→B→C cascade has no single owner who can observe that the whole frontier — every still-active hop in the cascade — is empty. A holds an edge to B; B holds an edge to C; no participant sees the others’ in-flight work. &lt;strong&gt;Quiescence is a global property of the mesh; a per-edge timeout is a local guess.&lt;/strong&gt; Term by term: stream-closure on one edge is &lt;em&gt;necessary&lt;/em&gt; for “the cascade settled,” and nowhere near &lt;em&gt;sufficient&lt;/em&gt;, because the edge that closed tells you nothing about the edge three hops downstream that is busiest right now. This is genuinely distributed termination detection, wearing an agent-protocol hat — a problem with decades of theory and no agent-protocol implementation. It’s hard, and the field skipped it for being hard. That is not a reason to keep skipping it.&lt;/p&gt;
&lt;h2 id=&quot;what-this-does-not-claim&quot;&gt;What this does not claim&lt;a class=&quot;heading-anchor&quot; href=&quot;#what-this-does-not-claim&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;span class=&quot;marginnote&quot; role=&quot;note&quot;&gt;One swallow is not summer. The fix makes one teardown deterministic; it does not make the mesh observable. Keeping that line honest is what separates a war story from a sales pitch.&lt;/span&gt;
&lt;p&gt;Keep the disclosed loss in view. The #1101 fix is real, tested, and closes one race in one process. It does &lt;em&gt;not&lt;/em&gt; hand the ecosystem the missing drain primitive. A correct &lt;code&gt;aclose()&lt;/code&gt; makes a single handler’s teardown deterministic; it does nothing to tell agent A that agent B’s push to C has landed. The in-process bug and the cross-network gap are the same shape at two zoom levels, but solving the first does not solve the second — and a piece that pretended otherwise would be the exact false-green it is warning about.&lt;/p&gt;
&lt;p&gt;That distinction is also why quiescence belongs as a first-class primitive, not a fifteen-second hope. Naming the gap is not solving it; the fix upstream and the primitive are two different commitments, and only one of them is a concrete, tested patch now sitting in a vendor SDK’s shipped code.&lt;/p&gt;
&lt;h2 id=&quot;the-rule-it-became&quot;&gt;The rule it became&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-rule-it-became&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;






























&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;what read green&lt;/th&gt;&lt;th&gt;what was actually true&lt;/th&gt;&lt;th&gt;the rule it became&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Shutdown exited &lt;code&gt;0&lt;/code&gt;, no traceback&lt;/td&gt;&lt;td&gt;A producer was left running; an in-flight delegation evaporated with the loop&lt;/td&gt;&lt;td&gt;Assert on &lt;strong&gt;drain completed&lt;/strong&gt;, never on an exit code returned&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;cancel()&lt;/code&gt; + &lt;code&gt;gather()&lt;/code&gt; under the lock looked careful&lt;/td&gt;&lt;td&gt;The tasks’ own teardown re-acquired that lock — instant deadlock&lt;/td&gt;&lt;td&gt;Snapshot under the lock; release it; cancel-and-gather &lt;em&gt;outside&lt;/em&gt; it&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;start()&lt;/code&gt; succeeded during teardown&lt;/td&gt;&lt;td&gt;A late spawn leaked a fresh producer past the barrier &lt;code&gt;aclose()&lt;/code&gt; closed&lt;/td&gt;&lt;td&gt;Set the &lt;code&gt;finished&lt;/code&gt; flag and check it under the &lt;em&gt;same&lt;/em&gt; lock — refuse, loudly&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;a2a-tck&lt;/code&gt; passed the push test after 15s&lt;/td&gt;&lt;td&gt;The clock advanced; the mesh’s actual settlement was never observed&lt;/td&gt;&lt;td&gt;Gate on an observed drain signal, never on a wall-clock timeout&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;If a planted in-flight task can survive your shutdown and your gate stays green, your teardown is decoration. That is the portable rule.&lt;/p&gt;
&lt;h2 id=&quot;read-it-yourself&quot;&gt;Read it yourself&lt;a class=&quot;heading-anchor&quot; href=&quot;#read-it-yourself&quot; aria-label=&quot;Link to this section&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The absence is checkable without taking my word for it. Three primary sources, one find-target each:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read &lt;a href=&quot;https://github.com/a2aproject/a2a-python/pull/1105&quot;&gt;PR #1105&lt;/a&gt; and find the lock release &lt;em&gt;before&lt;/em&gt; the &lt;code&gt;gather&lt;/code&gt; in &lt;code&gt;aclose()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read &lt;a href=&quot;https://a2a-protocol.org/latest/whats-new-v1/&quot;&gt;the v1 changelog&lt;/a&gt; and find the line that removes the per-task &lt;code&gt;final&lt;/code&gt; flag.&lt;/li&gt;
&lt;li&gt;Read &lt;a href=&quot;https://github.com/a2aproject/a2a-tck&quot;&gt;&lt;code&gt;a2a-tck&lt;/code&gt;&lt;/a&gt; and find the hardcoded fifteen-second fallback in the push-notification test.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three clicks, one conclusion: the agent ecosystem has named “the stream closed” and has not named “the mesh is done.”&lt;/p&gt;
&lt;p&gt;The in-process priors are next door. &lt;a href=&quot;https://thepragmaticquant.com/coding-agents-cant-hear-each-other/&quot;&gt;Coding agents can’t hear each other&lt;/a&gt; is the same blindness on one box — peers that cannot tell when a neighbor finishes or fails; this piece is the cross-process, cross-network sequel. &lt;a href=&quot;https://thepragmaticquant.com/green-is-not-evidence/&quot;&gt;Green is not evidence&lt;/a&gt; is the false-green lens this whole teardown wears. And the drain-before-return discipline the wire actually needs runs through &lt;a href=&quot;https://thepragmaticquant.com/source-to-subscriber-milliseconds/&quot;&gt;source to subscriber in milliseconds&lt;/a&gt;. The single box has a nervous system. The mesh does not yet have a way to know it has gone quiet — and the producer that died at shutdown is what that looks like in a stack trace.&lt;/p&gt;
&lt;section class=&quot;faq&quot; aria-label=&quot;Frequently asked questions&quot; data-astro-cid-z6gx6xcw&gt; &lt;h2 data-astro-cid-z6gx6xcw&gt;Frequently asked questions&lt;/h2&gt; &lt;div class=&quot;faq-list&quot; data-astro-cid-z6gx6xcw&gt; &lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;What is the &amp;#39;Task was destroyed but it is pending!&amp;#39; warning?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;It is a message CPython prints at event-loop finalization when it finds an asyncio Task still alive — still parked on an await it will never resume. It is usually benign exit-time noise, which is exactly why a real deadlock that prints it can hide in plain sight.&lt;/div&gt; &lt;/details&gt;&lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;What was the bug in a2a-python Issue #1101?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;During teardown, a background producer coroutine inside an ActiveTask was left running while the EventQueue subscriber it fed was torn down underneath it. The naive fix — cancel-and-gather while holding the registry lock — re-deadlocked, because each cancelled task&amp;#39;s own teardown re-acquires that same lock to deregister itself.&lt;/div&gt; &lt;/details&gt;&lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;What did PR #1105 change?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;It added a public async aclose() on DefaultRequestHandlerV2, ActiveTaskRegistry, and ActiveTask that closes the queues immediate=True, releases the registry lock before awaiting the gather, cancels-and-gathers the background tasks, and refuses to resurrect a finished task via a one-way finished state checked under the same lock. 417 added lines across eight files, 312 of them tests; CI green; merged upstream on 2026-07-20.&lt;/div&gt; &lt;/details&gt;&lt;details class=&quot;faq-item&quot; open data-astro-cid-z6gx6xcw&gt; &lt;summary data-astro-cid-z6gx6xcw&gt;What is the ecosystem-level gap the bug exposes?&lt;/summary&gt; &lt;div class=&quot;faq-a&quot; data-astro-cid-z6gx6xcw&gt;There is no sound way to know when an async A2A mesh has settled. The v1.0 spec removed the per-task final flag, no SDK exposes a wait_for_idle or drain primitive, and the a2a-tck conformance kit falls back to a hardcoded 15-second timeout. That is distributed termination detection, and the agent-protocol stack has no name for it yet.&lt;/div&gt; &lt;/details&gt; &lt;/div&gt; &lt;script type=&quot;application/ld+json&quot;&gt;{&quot;@context&quot;:&quot;https://schema.org&quot;,&quot;@type&quot;:&quot;FAQPage&quot;,&quot;mainEntity&quot;:[{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;What is the &apos;Task was destroyed but it is pending!&apos; warning?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;It is a message CPython prints at event-loop finalization when it finds an asyncio Task still alive — still parked on an await it will never resume. It is usually benign exit-time noise, which is exactly why a real deadlock that prints it can hide in plain sight.&quot;}},{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;What was the bug in a2a-python Issue #1101?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;During teardown, a background producer coroutine inside an ActiveTask was left running while the EventQueue subscriber it fed was torn down underneath it. The naive fix — cancel-and-gather while holding the registry lock — re-deadlocked, because each cancelled task&apos;s own teardown re-acquires that same lock to deregister itself.&quot;}},{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;What did PR #1105 change?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;It added a public async aclose() on DefaultRequestHandlerV2, ActiveTaskRegistry, and ActiveTask that closes the queues immediate=True, releases the registry lock before awaiting the gather, cancels-and-gathers the background tasks, and refuses to resurrect a finished task via a one-way finished state checked under the same lock. 417 added lines across eight files, 312 of them tests; CI green; merged upstream on 2026-07-20.&quot;}},{&quot;@type&quot;:&quot;Question&quot;,&quot;name&quot;:&quot;What is the ecosystem-level gap the bug exposes?&quot;,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;There is no sound way to know when an async A2A mesh has settled. The v1.0 spec removed the per-task final flag, no SDK exposes a wait_for_idle or drain primitive, and the a2a-tck conformance kit falls back to a hardcoded 15-second timeout. That is distributed termination detection, and the agent-protocol stack has no name for it yet.&quot;}}]}&lt;/script&gt; &lt;/section&gt; &lt;!-- Collapsed-by-default FAQ (operator ruling 2026-07-15, supersedes the F-40
     mobile-only collapse): each Q/A pair is a &lt;details&gt;, server-rendered OPEN so
     the answers are visible with no JS, in print, and to crawlers (the full text
     is always in the DOM; the FAQPage JSON-LD above is untouched). This script
     collapses every pair on load AT ALL WIDTHS so the questions read as a
     scannable index and an answer expands on click. astro:page-load covers
     view-transition navigations. --&gt; &lt;script type=&quot;module&quot; src=&quot;https://thepragmaticquant.com/vercel/path0/src/components/Faq.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;</content:encoded><category>a2a</category><category>agents</category><category>structured-concurrency</category><category>the problem</category></item></channel></rss>