<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Miquel Puig Turon</title>
    <link>https://miquelpuigturon.com/</link>
    <description>The whole site, root to leaf: every article lives on a path in a tree of thoughts, and new or changed pages arrive here in full.</description>
    <language>en</language>
    <lastBuildDate>Thu, 08 Oct 2026 13:07:43 GMT</lastBuildDate>
    <atom:link href="https://miquelpuigturon.com/rss.xml" rel="self" type="application/rss+xml" />
  <item>
    <title>Humanity</title>
    <link>https://miquelpuigturon.com/humanity/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Humanity Tangible things that might aid other people. Possibly unbeknownst to me. I strive to have a positive effect on humanity. Short term, it will likely be outcomes of my work. Long term, it…</description>
    <content:encoded>&lt;h1 id=&quot;humanity&quot;&gt;Humanity&lt;/h1&gt;
&lt;p&gt;Tangible things that might aid other people. Possibly unbeknownst to me.&lt;/p&gt;
&lt;p&gt;I strive to have a positive effect on humanity.&lt;/p&gt;
&lt;p&gt;Short term, it will likely be outcomes of my work. &lt;br&gt;
Long term, it might document project(s) that I build.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Self</title>
    <link>https://miquelpuigturon.com/self/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/self/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Self Beliefs around how I live my life. Or maybe attempts at explaining why. I will write about the things that are most unique to me. It is not intended to be a guide to follow, but as a description…</description>
    <content:encoded>&lt;h1 id=&quot;self&quot;&gt;Self&lt;/h1&gt;
&lt;p&gt;Beliefs around how I live my life.&lt;/p&gt;
&lt;p&gt;Or maybe attempts at explaining why.&lt;/p&gt;
&lt;p&gt;I will write about the things that are most unique to me. &lt;br&gt;
It is not intended to be a guide to follow, but as a description to understand.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Welcome</title>
    <link>https://miquelpuigturon.com/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Welcome This is my personal site. A caricature of my thoughts. Why I’ve lived most of my life feeling misunderstood. Partly because my convictions differ so much from what’s considered normal. Mostly…</description>
    <content:encoded>&lt;h1 id=&quot;welcome&quot;&gt;Welcome&lt;/h1&gt;
&lt;p&gt;This is my personal site. &lt;br&gt;
     A caricature of my thoughts.&lt;/p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;/h2&gt;
&lt;p&gt;I’ve lived most of my life feeling misunderstood.&lt;/p&gt;
&lt;p&gt;Partly because my convictions differ so much from what’s considered normal. &lt;br&gt;
Mostly because of my inability to express my understanding of the world in simple words.&lt;/p&gt;
&lt;p&gt;Perfection has always been an inquietude within me, yet forever beyond reach. &lt;br&gt;
By definition, perfection is immutable, and I’ll always grow. Still, some ideas are deeply rooted in me.&lt;/p&gt;
&lt;p&gt;This is an attempt to understand.&lt;/p&gt;
&lt;h2 id=&quot;what&quot;&gt;What&lt;/h2&gt;
&lt;p&gt;I intend for this website to be a dump of my brain.&lt;/p&gt;
&lt;p&gt;Not a blog, not a collection of anecdotes. A fully-connected network of thoughts.&lt;/p&gt;
&lt;p&gt;Each article lives on a path, making this page the root of a tree. &lt;br&gt;
To comprehend why and what is written in an article, its path and siblings matter.&lt;/p&gt;
&lt;p&gt;This is an attempt at perfection.&lt;/p&gt;
&lt;h2 id=&quot;where-to-start&quot;&gt;Where to start&lt;/h2&gt;
&lt;p&gt;If you’re not sure which to read, three pieces stand on their own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/self/pain-suffering-and-god/&quot;&gt;Pain, Suffering and God&lt;/a&gt; — why I build, and what it costs.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/humanity/orkava/&quot;&gt;Tres mesos intentant construir software per a farmàcies&lt;/a&gt; — the full autopsy of a startup that didn’t work (in Catalan).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/self/sport/boxing/&quot;&gt;Boxing&lt;/a&gt; — a year of getting hit, and what it settled.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you’re done, the whole tree is right below — broader near the root, specifics deeper down.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;em&gt;See how &lt;a href=&quot;/meta/&quot;&gt;this page&lt;/a&gt; was built.&lt;/em&gt;&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Fire</title>
    <link>https://miquelpuigturon.com/humanity/fire/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/fire/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Fire My thoughts building :fire:</description>
    <content:encoded>&lt;h1 id=&quot;fire&quot;&gt;Fire&lt;/h1&gt;
&lt;p&gt;My thoughts building &lt;a href=&quot;https://firedash.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;:fire:&lt;/a&gt;&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Money</title>
    <link>https://miquelpuigturon.com/humanity/money/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/money/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Money I believe few companies will monopolize the market. Money will mostly become meaningless, and almost everyone will both live in abundance and be ridiculously poor. How money grows For most…</description>
    <content:encoded>&lt;h1 id=&quot;money&quot;&gt;Money&lt;/h1&gt;
&lt;p&gt;I believe few companies will monopolize the market.&lt;/p&gt;
&lt;p&gt;Money will mostly become meaningless, and almost everyone will both live in abundance and be ridiculously poor.&lt;/p&gt;
&lt;h2 id=&quot;how-money-grows&quot;&gt;How money grows&lt;/h2&gt;
&lt;p&gt;For most people, money grows through salary and goes away via day to day expenses.&lt;/p&gt;
&lt;p&gt;Salaries grow slower than assets, and this compounds.&lt;/p&gt;
&lt;p&gt;It is very hard to jump an order of magnitude.&lt;/p&gt;
&lt;p&gt;If someone starts from scratch, it will likely take its entire live to acquire 1M$ of assets.
If another starts from 1M$ of assets, it will likely end its live with 100M$ of assets.&lt;/p&gt;
&lt;p&gt;The math is simple, put your money in any vehicle that returns in proportion to its money:
&lt;code&gt;d$/dt = k$ =&gt; $(t) = $_0 * e^kt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;To maintain the money difference with someone, you need to grow your money at the same rate he does.&lt;br&gt;
If you add to your wealth X% MoM, in an infinite amount of time, you will be wealthier than anyone who adds less, no matter the initial amount.&lt;/p&gt;
&lt;h2 id=&quot;why-non-asset-owners-wont-be-rich&quot;&gt;Why non-asset owners won’t be rich&lt;/h2&gt;
&lt;p&gt;Given a modest start, you &lt;em&gt;need&lt;/em&gt; to work to stay alive.&lt;/p&gt;
&lt;p&gt;If you save a fixed amount, you might reach a point where returns from your assets are higher than your salary.&lt;br&gt;
You can stop working and stop adding money, or keep a job to pay for your expenses and let money grow.&lt;br&gt;
At some point, you might have enough assets that income from your job will be a rounding error and then you are rich.&lt;/p&gt;
&lt;p&gt;Notice though, how default state for those who do not own assets is to survive vs default state for asset owners, which is to compound wealth.&lt;br&gt;
This means that by default, the gap between rich and poor grows bigger.&lt;/p&gt;
&lt;p&gt;We will reach a point where salary will not be enough to get you to own assets. (I believe we are not far from it)&lt;/p&gt;
&lt;h2 id=&quot;why-inheritance-sucks&quot;&gt;Why inheritance sucks&lt;/h2&gt;
&lt;p&gt;In a world where we are slaves of abundance, assets will only be able to propagate through inheritance.&lt;/p&gt;
&lt;p&gt;As the positions to get rich from nothing grow thinner, and needs are abundantly met, there’s no point in effort.&lt;/p&gt;
&lt;p&gt;Why would people push themselves when there is no reward?&lt;/p&gt;
&lt;p&gt;I think the only fair way to redistribute assets is killing inheritance.&lt;/p&gt;
&lt;p&gt;My ideas go much further. I think education should be globally homogeneous and parent’s money should not influence it, else we are propagating wealth through education.&lt;br&gt;
But that’s writing for another day.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Tres mesos intentant construir software per a farmàcies</title>
    <link>https://miquelpuigturon.com/humanity/orkava/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/orkava/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Tres mesos intentant construir software per a farmàcies Des de que vaig escriure aquest article, ha cambiat la situació. Orkava ha pivotat lleguerament, i tira endavant. Deixo l&apos;article fins que el…</description>
    <content:encoded>&lt;h1 id=&quot;tres-mesos-intentant-construir-software-per-a-farmàcies&quot;&gt;Tres mesos intentant construir software per a farmàcies&lt;/h1&gt;
&lt;aside class=&quot;callout&quot;&gt;
Des de que vaig escriure aquest article, ha cambiat la situació. Orkava ha pivotat lleguerament, i tira endavant. Deixo l&apos;article fins que el reescrigui.
&lt;/aside&gt;
&lt;p&gt;Hem entrevistat unes 70 farmàcies, hem instal·lat el producte a 12 i hem acabat concloent que el model de negoci en que crèiem no funciona.&lt;/p&gt;
&lt;h2 id=&quot;per-què-farmàcia&quot;&gt;Per què farmàcia&lt;/h2&gt;
&lt;p&gt;En Lluís és amic meu, fill de farmacèutics, ha passat tota la vida a la farmàcia. Em va explicar els seus problemes, idees i el panorama actual del software. Jo hi vaig veure l’oportunitat d’aportar valor.&lt;/p&gt;
&lt;h2 id=&quot;el-mercat-de-la-farmàcia&quot;&gt;El mercat de la farmàcia&lt;/h2&gt;
&lt;p&gt;La farmàcia és un negoci de marge baix que cada cop és més difícil de defensar. Hi ha dos camins que les farmàcies estan prenent per mantenir-se competitives.&lt;/p&gt;
&lt;h3 id=&quot;volum-i-marge&quot;&gt;Volum i marge&lt;/h3&gt;
&lt;p&gt;Una estratègia en augment a Europa, i amb gran èxit a França, és unir la farmàcia a un grup. Els grups de compra tenen més poder de negociació que les farmàcies independents, i per tant costos d’adquisició més baixos. Més enllà dels grups de compra hi ha les “marques”, que afegeixen imatge, consultoria i eines de gestió.&lt;/p&gt;
&lt;p&gt;Tots dos models són atractius per a la farmàcia perquè simplifiquen la gestió i ajuden a pujar el sell-out. En una situació on la gestió integral requereix cada cop més feina i el retorn és més difícil d’aconseguir, és fàcil entendre per què creixen.&lt;/p&gt;
&lt;h3 id=&quot;diferenciació-i-serveis&quot;&gt;Diferenciació i serveis&lt;/h3&gt;
&lt;p&gt;La farmàcia cada vegada més es posiciona com a punt de referència per fer molt més que dispensar: oferir serveis. Ortopèdia, anàlisi de paràmetres biomèdics, SPD… Això ve impulsat des de la pròpia farmàcia i des d’organitzacions governamentals, que volen reduir feina de les institucions mèdiques i convertir la farmàcia en referent sanitari.&lt;/p&gt;
&lt;h2 id=&quot;orkava&quot;&gt;Orkava&lt;/h2&gt;
&lt;p&gt;El farmacèutic té poc temps i ha de fer mil coses, i sovint no té els processos optimitzats: gestió de stock, substitució de genèrics, cross-selling, fidelització de pacients… Orkava neix per ajudar amb tot això. La resta és la història de com hem anat canviant d’hipòtesi per aconseguir-ho.&lt;/p&gt;
&lt;h2 id=&quot;què-hem-après&quot;&gt;Què hem après&lt;/h2&gt;
&lt;p&gt;Quan vam començar, pensàvem que el farmacèutic necessitava una capa d’intel·ligència sobre l’ERP per prendre millors decisions.&lt;/p&gt;
&lt;p&gt;Vam atacar els processos més evidents: gestió de stock, substitució de genèrics, cross-selling, fidelització de pacients, tasques internes i horaris. Totes aquestes funcionalitats aporten valor, però cap forma part del dia a dia. Es consulten quan hi ha un dubte concret, però no generen hàbit.&lt;/p&gt;
&lt;p&gt;Ho vam confirmar amb els primers resultats: hem parlat amb unes 70 farmàcies i n’hem instal·lat 12 amb seguiment personalitzat. D’aquestes, només 4 l’usen a diari, 5 setmanalment i 3 no l’usen. Cap explota Orkava com imaginàvem.&lt;/p&gt;
&lt;p&gt;Vam intentar reduir la fricció posant-ho tot dins d’un xat. Ha estat útil per a preguntes concretes, com analitzar una marca o entendre una baixada de marge, però no ha creat pull. El farmacèutic només obre el xat quan ja té una pregunta, i això passa poc.&lt;/p&gt;
&lt;p&gt;Després vam fer Orkava més proactiu amb notificacions sobre impagats, stock parat, caducitats o altres incidències. Tampoc va funcionar: moltes alertes no porten a una acció clara i és difícil demostrar valor sense mesurar si l’acció s’ha fet ni quin impacte ha tingut.&lt;/p&gt;
&lt;p&gt;El mòdul de màrqueting va seguir el mateix patró. Les farmàcies que ja feien màrqueting el van aprofitar i no volien cambiar; les que no, no van crear nous processos.&lt;/p&gt;
&lt;p&gt;La següent hipòtesi va ser que estàvem fora del flux de treball. Si volíem formar part del dia a dia, havíem d’estar presents en el moment de la venda. Integrar-nos amb l’ERP va ser molt més difícil del que esperàvem, però ho vam aconseguir per altres vies :).&lt;/p&gt;
&lt;p&gt;El resultat tampoc va ser suficient. Els popups semblen útils al principi, però ràpidament es converteixen en una cosa més a la pantalla. Les recomanacions centrades en el pacient i el seu historial aporten més valor que els suggeriments típics de producte, però no vam veure prou evidència que poguessin convertir-se en una part indispensable de la venda.&lt;/p&gt;
&lt;p&gt;La conclusió és que no hem aconseguit entrar en cap workflow indispensable del farmacèutic. Hem construït una capa d’intel·ligència per a la farmàcia, però depèn que el farmacèutic decideixi consultar-la. I aquest és precisament el problema: si el producte no forma part d’un procés obligatori, acaba sent una eina que sembla útil, però que cada vegada s’utilitza menys.&lt;/p&gt;
&lt;h2 id=&quot;els-competidors&quot;&gt;Els competidors&lt;/h2&gt;
&lt;p&gt;El producte més similar a Orkava que està ben posicionat és Sisfarma. Es posicionen com el CRM de la farmàcia. La webapp és una castanya, i els farmacèutics amb qui hem parlat que el tenen no la fan servir.&lt;/p&gt;
&lt;p&gt;Com han aconseguit entrar al mercat? Perquè en realitat són una consultoria.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NO ESTÁS SOLO:
TECNOLOGÍA CON ALMA DE CONSULTORÍA&lt;/p&gt;
&lt;p&gt;Sisfarma no es solo un software. Ponemos a tu disposición un equipo de asesores expertos en rentabilidad con los que te reunirás cada mes para analizar tu farmacia, fijar estrategias y garantizar resultados.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;La consultoria funciona al món de la farmàcia, i hi ha diversos jugadors. Sisfarma ho ha fet molt bé: en construir la plataforma, amb menys dedicació i personal poden oferir una consultoria suficient.&lt;/p&gt;
&lt;p&gt;Intelia Pharma posa un popup que et recomana productes en funció d’altres productes. Les farmàcies amb qui hem parlat que el tenen estan descontentes: és car i s’acaba ignorant. Crec que el seu enfocament és erroni, perquè parteix del producte i no de l’historial del pacient.&lt;/p&gt;
&lt;p&gt;No hi ha cap jugador que hagi aconseguit entrar al mercat posant-se en el moment de la venda.&lt;/p&gt;
&lt;p&gt;Menció a Blistersuite, que ha trobat un flux que el farmacèutic ha de fer per nova normativa i que no està integrat a l’ERP. El programa deixa molt a desitjar, però fa la seva funció i ha capturat el mercat (que és petit).&lt;/p&gt;
&lt;h2 id=&quot;oportunitats&quot;&gt;Oportunitats&lt;/h2&gt;
&lt;p&gt;L’oportunitat més clara és competir amb Sisfarma amb un model de service-as-software: convertir-se en una consultoria i intentar optimitzar amb IA tots els processos.&lt;/p&gt;
&lt;p&gt;Crec personalment que l’anàlisi i la potenciació de la venda podrien crear mercat, però requeririen ser invisibles: àudio a text, ulleres intel·ligents… I tot això implica passar informació del pacient a tercers i obtenir-ne el consentiment, especialment delicat perquè es parla de salut. Legalment impossible.&lt;/p&gt;
&lt;p&gt;No he explorat el tooling per a grups de farmàcies. Crec que aniran creixent i que pràcticament desapareixeran les farmàcies realment independents, de manera que hi ha un mercat emergent (comparatives entre farmàcies del mateix grup, benchmarks, gestió centralitzada de marques pròpies, stocks, reposicions…).&lt;/p&gt;
&lt;p&gt;Una altra opció és posicionar-se del costat del laboratori o la marca. Tenen pressupost per educar el farmacèutic i per interactuar-hi comercialment. Es pot intentar automatitzar aquesta feina, o fins i tot a donar eines al farmacèutic (finançades pel laboratori) per treure fricció, facilitar processos o augmentar-ne el coneixement.&lt;/p&gt;
&lt;p&gt;I, finalment, construir un ERP modern. El que caldrà és esperar uns anys, que hi hagi relleu generacional i que passin les regulacions del col·legi de farmacèutics. Segurament vindrà per part del propi ERP.&lt;/p&gt;
&lt;h2 id=&quot;conclusió&quot;&gt;Conclusió&lt;/h2&gt;
&lt;p&gt;A la farmàcia, s’adopta tecnologia quan resol un workflow obligatori: l’ERP per la recepta electrònica, Blistersuite per la regulació.&lt;/p&gt;
&lt;p&gt;No veig cap producte sense consultoria que actualment pugui crear mercat en aquest espai. Crec que, en uns anys, els ERP aniran introduint aquests fluxos, però és molt difícil competir-hi.&lt;/p&gt;
&lt;p&gt;Orkava ha mort. Tenim l’oportunitat d’entrar al mercat i substituir Sisfarma en algunes farmàcies, però des del principi vam decidir que no seriem consultors.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Temporal</title>
    <link>https://miquelpuigturon.com/humanity/temporal/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/temporal/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Temporal Useful (miss/un)documented articles for working with Temporal .</description>
    <content:encoded>&lt;h1 id=&quot;temporal&quot;&gt;Temporal&lt;/h1&gt;
&lt;p&gt;Useful (miss/un)documented articles for working with &lt;a href=&quot;https://temporal.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Temporal&lt;/a&gt;.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Addictions</title>
    <link>https://miquelpuigturon.com/self/addictions/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/self/addictions/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Addictions Addictions determine who you are. The fight against them shapes who you will become. Why I avoid them Because they take from me things I am not willing to give up: Desire for greatness I…</description>
    <content:encoded>&lt;h1 id=&quot;addictions&quot;&gt;Addictions&lt;/h1&gt;
&lt;p&gt;Addictions determine who you are. The fight against them shapes who you will become.&lt;/p&gt;
&lt;h2 id=&quot;why-i-avoid-them&quot;&gt;Why I avoid them&lt;/h2&gt;
&lt;p&gt;Because they take from me things I am not willing to give up:&lt;/p&gt;
&lt;h3 id=&quot;desire-for-greatness&quot;&gt;Desire for greatness&lt;/h3&gt;
&lt;p&gt;I want to be great. I believe I can be, I just need to keep improving.&lt;/p&gt;
&lt;p&gt;Growth lies in discomfort and small work that compounds. These require boredom and thought. &lt;br&gt;
Dreams fall short under the urgency to fulfill the present moment.&lt;/p&gt;
&lt;h3 id=&quot;innocent-belief-in-perfection&quot;&gt;Innocent belief in perfection&lt;/h3&gt;
&lt;p&gt;I resonate with Plato’s idealism. I have always been distressed by the lack of perfection. Plato’s Forms are perfect and unchanging.&lt;/p&gt;
&lt;p&gt;Addictions have no existence of their own. You are a vessel of their existence.&lt;/p&gt;
&lt;h3 id=&quot;novelty&quot;&gt;Novelty&lt;/h3&gt;
&lt;p&gt;Addictions are thieves of novelty. We disregard everything to bathe in their embrace.&lt;/p&gt;
&lt;p&gt;I’ve seen it happen with weed. A friend stops showing up. Why go out when you already know what will make you feel good?&lt;/p&gt;
&lt;h2 id=&quot;some-not-so-obvious-addictions&quot;&gt;Some not-so-obvious addictions&lt;/h2&gt;
&lt;h3 id=&quot;porn&quot;&gt;Porn&lt;/h3&gt;
&lt;p&gt;There is a societal consensus that tells us it’s okay. &lt;br&gt;
Most of my friends talk about it with shame, that it doesn’t feel okay. On finish, there’s repulsion.&lt;/p&gt;
&lt;p&gt;I quit porn around a year ago. I simply decided to stop. I read &lt;a href=&quot;https://easypeasymethod.org/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this book&lt;/a&gt;. &lt;br&gt;
Since then, I’ve been at my best physically, and my happiness has increased. It might be cause or effect.&lt;/p&gt;
&lt;h3 id=&quot;coffee&quot;&gt;Coffee&lt;/h3&gt;
&lt;p&gt;Coffee is the most socially defended addiction.&lt;/p&gt;
&lt;p&gt;Most daily consumers deny that they are addicted, yet openly attribute their mood, productivity, and patience to whether they have had it.&lt;/p&gt;
&lt;p&gt;The usual defense is that it’s not bad for your health. This misses the point. Harm is not what makes an addiction worth avoiding.&lt;/p&gt;
&lt;h3 id=&quot;love&quot;&gt;Love&lt;/h3&gt;
&lt;p&gt;Being in love is the strongest addiction I have ever experienced. It robs you of everything, only that one girl matters.&lt;/p&gt;
&lt;p&gt;If mutual, this is the only addiction I would welcome. Even knowing the cost.&lt;/p&gt;
&lt;h3 id=&quot;digital-consumption&quot;&gt;Digital Consumption&lt;/h3&gt;
&lt;p&gt;Likely the addiction that most affects our generation. I used to watch Instagram every night before going to sleep, as if it were a ritual. I now am addicted to YouTube.&lt;/p&gt;
&lt;p&gt;Everyone participates in it. It robs you of attention. A quiet moment comes, and you fill it before it begins.&lt;/p&gt;
&lt;h2 id=&quot;fighting-addictions&quot;&gt;Fighting addictions&lt;/h2&gt;
&lt;p&gt;My beliefs are strong. My will is weak. If something relies on my willpower, I will fail.&lt;/p&gt;
&lt;h3 id=&quot;avoiding-temptation&quot;&gt;Avoiding temptation&lt;/h3&gt;
&lt;p&gt;The best way to fight addictions is not having to. It’s hard to not eat a cookie, it’s much easier to not buy it.&lt;/p&gt;
&lt;h3 id=&quot;self-honesty&quot;&gt;Self-honesty&lt;/h3&gt;
&lt;p&gt;Do not attempt to ‘reduce’ an addiction, to drink less coffee, to watch porn only every x days. Moderation is self-deception.&lt;/p&gt;
&lt;p&gt;Quitting requires a decision and acknowledgement that you were an addict. Note the past tense.&lt;/p&gt;
&lt;h3 id=&quot;compounding-effect&quot;&gt;Compounding effect&lt;/h3&gt;
&lt;p&gt;Addictions compound. One must always be vigilant, keeping addictions at bay. It’s easy once you fall back to one to wish for another.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;p&gt;I have to fight less every day. &lt;br&gt;
I’m building a life where addictions don’t belong.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>How I Want to Live My Life</title>
    <link>https://miquelpuigturon.com/self/how-i-want-to-live-my-life/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/self/how-i-want-to-live-my-life/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>How I Want to Live My Life Why am I writing this I have a burning desire to do more. To be better. I think I have always had it, but it was silenced over the years. Now it’s roaring back. No matter…</description>
    <content:encoded>&lt;h1 id=&quot;how-i-want-to-live-my-life&quot;&gt;How I Want to Live My Life&lt;/h1&gt;
&lt;h2 id=&quot;why-am-i-writing-this&quot;&gt;Why am I writing this&lt;/h2&gt;
&lt;p&gt;I have a burning desire to do more. To be better. &lt;br&gt;
I think I have always had it, but it was silenced over the years. Now it’s roaring back.&lt;/p&gt;
&lt;p&gt;No matter how many times I lose, I know I am going to win.&lt;/p&gt;
&lt;h2 id=&quot;how-my-life-has-been-up-to-now&quot;&gt;How my life has been up to now&lt;/h2&gt;
&lt;p&gt;When I was young, I got used to being good at everything.&lt;/p&gt;
&lt;p&gt;I was talented and lacked ambition, so I didn’t put in the effort.&lt;/p&gt;
&lt;p&gt;When I went to uni I expected it to be hard. It wasn’t.&lt;/p&gt;
&lt;p&gt;At that point in my life I couldn’t care about anything.&lt;br&gt;
I was unhappy.&lt;/p&gt;
&lt;p&gt;Then I fell in love. I failed hard.&lt;br&gt;
It made me happy.&lt;/p&gt;
&lt;p&gt;Failure was a hard concept to grasp. Now I pursue it deliberately through physical effort.&lt;/p&gt;
&lt;p&gt;I have not had a clear path forward. I tumbled from one thing to another until my current job.&lt;br&gt;
I dedicated a year to it, every day, all day. I am proud of the work done.&lt;/p&gt;
&lt;p&gt;But now that I know what I want, I am trapped by its success. I can either wait, or pursue my path and walk away from the reward.&lt;/p&gt;
&lt;h3 id=&quot;physical-effort&quot;&gt;Physical Effort&lt;/h3&gt;
&lt;p&gt;I used to think that achieving without effort made me better.&lt;/p&gt;
&lt;p&gt;I was wrong. I have fallen in love with pain.&lt;br&gt;
I now push myself for the effort itself.&lt;/p&gt;
&lt;h3 id=&quot;coding&quot;&gt;Coding&lt;/h3&gt;
&lt;p&gt;I came to hate coding.&lt;/p&gt;
&lt;p&gt;There has been a breakthrough since. &lt;em&gt;AI&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Coding &lt;a href=&quot;https://github.com/jamipouchi/Miquel&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this site&lt;/a&gt; has been so much fun. Building &lt;a href=&quot;https://github.com/embrake-AI/fire&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;fire&lt;/a&gt; is a joy.&lt;/p&gt;
&lt;h2 id=&quot;happiness&quot;&gt;Happiness&lt;/h2&gt;
&lt;p&gt;Happiness is a pursuit that dies on its existence.&lt;/p&gt;
&lt;p&gt;I have only found it through commitment.&lt;/p&gt;
&lt;h2 id=&quot;how-i-will-do-it&quot;&gt;How I will do it&lt;/h2&gt;
&lt;p&gt;The only thing that matters is winning.&lt;br&gt;
We can add artificial constraints, but those are only visible to oneself.&lt;/p&gt;
&lt;p&gt;A goal has to be bold enough to seem unbelievable to most people yet still reachable in your own mind.&lt;/p&gt;
&lt;p&gt;This past year I started setting goals. So far I have mostly failed.&lt;br&gt;
But I will not stop.&lt;/p&gt;
&lt;p&gt;Most people live their lives without ever winning.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I started &lt;a href=&quot;https://workout-tracker.miquelpuigturon.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;tracking my workouts&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;dream-state&quot;&gt;Dream state&lt;/h2&gt;
&lt;p&gt;If you don’t choose a direction, you will end up living under someone else’s dreams.&lt;/p&gt;
&lt;p&gt;For most of my life, I’ve been guided by what I didn’t want to do.&lt;br&gt;
That’s inefficient. There are an infinite number of options to reject, but only one to choose.&lt;/p&gt;
&lt;p&gt;My lifetime goals are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To make a meaningful positive contribution to humanity&lt;/li&gt;
&lt;li&gt;To live in complete freedom&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is hard to choose a path that combines both, and I am uncertain how or with which to start.&lt;/p&gt;
&lt;p&gt;These goals are not unconditional, they are constrained by &lt;em&gt;fairness&lt;/em&gt;. &lt;br&gt;
I want to give back to those who deserve it.&lt;/p&gt;
&lt;p&gt;I want to keep the fire burning.&lt;/p&gt;
&lt;p&gt;Most days feel like I do something I don’t want, after doing something I didn’t want.&lt;/p&gt;
&lt;p&gt;Many people optimize for comfort in the present.&lt;br&gt;
I have my sight set at eternity.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Meta</title>
    <link>https://miquelpuigturon.com/meta/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/meta/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Meta This page explains how this site is built and structured. Philosophy This site is designed to be a personal knowledge base — a collection of thoughts, ideas, and reflections organized…</description>
    <content:encoded>&lt;h1 id=&quot;meta&quot;&gt;Meta&lt;/h1&gt;
&lt;p&gt;This page explains how this site is built and structured.&lt;/p&gt;
&lt;h2 id=&quot;philosophy&quot;&gt;Philosophy&lt;/h2&gt;
&lt;p&gt;This site is designed to be a personal knowledge base — a collection of thoughts, ideas, and reflections organized hierarchically by topic. The design philosophy emphasizes simplicity: write markdown, organize by folders, and the site structure emerges naturally.&lt;/p&gt;
&lt;h2 id=&quot;technical-stack&quot;&gt;Technical Stack&lt;/h2&gt;
&lt;p&gt;Built with &lt;a href=&quot;https://astro.build/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Astro&lt;/a&gt; using content collections. No complex CMS, no database — just markdown files in a folder structure.&lt;/p&gt;
&lt;p&gt;Using Cloudflare’s services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deployed as a static website to &lt;a href=&quot;https://developers.cloudflare.com/workers/static-assets/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare Workers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Comments are stored in &lt;a href=&quot;https://developers.cloudflare.com/r2/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare R2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AI to verify comments is provided by &lt;a href=&quot;https://developers.cloudflare.com/workers-ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Users and subscriptions are stored in a &lt;a href=&quot;https://developers.cloudflare.com/d1/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare D1&lt;/a&gt; database&lt;/li&gt;
&lt;li&gt;Create subscription and comment endpoints are served by a &lt;a href=&quot;https://developers.cloudflare.com/workers/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare Worker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Search and AI answers are powered by &lt;a href=&quot;https://developers.cloudflare.com/ai-search/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare AI Search&lt;/a&gt;, which crawls and indexes this site automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;content-structure&quot;&gt;Content Structure&lt;/h2&gt;
&lt;p&gt;All content lives in &lt;code&gt;src/content/pages/&lt;/code&gt; as markdown files:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; 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;src/content/pages/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── index.md           → / (home)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── self/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── index.md       → /self/ (branch page)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    └── addictions/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        └── index.md       → /self/addictions (leaf page)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── humanity/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── index.md       → /humanity/ (branch page)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    └── temporal/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        ├── index.md       → /humanity/temporal/ (branch page)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Folder structure = site structure.&lt;/strong&gt; No configuration needed.&lt;/p&gt;
&lt;h2 id=&quot;automatic-routing&quot;&gt;Automatic Routing&lt;/h2&gt;
&lt;p&gt;Three route files handle all pages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;index.astro&lt;/code&gt; - Home page with custom layout (&lt;code&gt;/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[...slug].astro&lt;/code&gt; - All content pages with unified layout (including this meta page!)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;404.astro&lt;/code&gt; - Missing pages&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;getting-around&quot;&gt;Getting Around&lt;/h2&gt;
&lt;p&gt;Everything here is one tree, and a page’s path is part of its meaning: broader topics near the root (&lt;code&gt;/humanity/&lt;/code&gt;), specifics deeper down (&lt;code&gt;/humanity/temporal/worker-configuration/&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Finding your bearings.&lt;/strong&gt; Breadcrumbs at the top of every page (Home / section / … / where you are), a small tree at the end of every page showing the current page among its siblings and children, and the &lt;a href=&quot;/#site-tree&quot;&gt;whole tree on the home page&lt;/a&gt;. The tree block on these site-internal pages only ever shows other meta pages — the section keeps to its own corner.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subscribing.&lt;/strong&gt; The form at the end of every page defaults to &lt;em&gt;everything — the whole site&lt;/em&gt;; you can narrow it to a single branch (that page and everything under it) or to just one article. Emails are stored encrypted and used for nothing else. To unsubscribe, email &lt;a href=&quot;mailto:miquel@miquelpuigturon.com&quot;&gt;miquel@miquelpuigturon.com&lt;/a&gt;. If you prefer feeds, every page arrives in full at &lt;a href=&quot;/rss.xml&quot;&gt;/rss.xml&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;design-choices&quot;&gt;Design Choices&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;No dark mode&lt;/strong&gt;: We are professionals, not guys coding under the covers of their bed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hierarchy&lt;/strong&gt;: Visual hierarchy matches content hierarchy — home is special, branches organize, leaves contain&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static&lt;/strong&gt;: Everything is served statically. Comments too.&lt;/p&gt;
&lt;h2 id=&quot;source&quot;&gt;Source&lt;/h2&gt;
&lt;p&gt;The original implementation of this site was built with cursor through a conversation with Claude 4.5 sonnet. You can read the &lt;a href=&quot;/meta/chat-transcript/&quot;&gt;full chat transcript&lt;/a&gt; to see how it was designed and implemented.&lt;/p&gt;
&lt;p&gt;Further history can be seen at &lt;a href=&quot;https://github.com/jamipouchi/Miquel&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/jamipouchi/Miquel&lt;/a&gt;.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>2026</title>
    <link>https://miquelpuigturon.com/meta/2026/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/meta/2026/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>2026 This will be the third year I set goals for myself for the year to come. I want them to be ambitious up to the point where I can only achieve them with dedication throughout the year. Goals…</description>
    <content:encoded>&lt;h1 id=&quot;2026&quot;&gt;2026&lt;/h1&gt;
&lt;p&gt;This will be the third year I set goals for myself for the year to come.&lt;/p&gt;
&lt;p&gt;I want them to be ambitious up to the point where I can only achieve them with dedication throughout the year.&lt;/p&gt;
&lt;h2 id=&quot;goals&quot;&gt;Goals&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Bench 110kg&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Run a sub-90min half-marathon&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both goals are easy to measure and to work towards. &lt;br&gt;
Additionally, I want to invest more time in building projects and relationships.&lt;/p&gt;
&lt;p&gt;Oh, I also won’t drink alcohol.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Sport</title>
    <link>https://miquelpuigturon.com/self/sport/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/self/sport/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Sport I strive for sport to be part of my live Past Growing up I played tennis. I was good, competed at province level. I disliked training, but I loved competing. When it got to either train daily…</description>
    <content:encoded>&lt;h1 id=&quot;sport&quot;&gt;Sport&lt;/h1&gt;
&lt;p&gt;I strive for sport to be part of my live&lt;/p&gt;
&lt;h2 id=&quot;past&quot;&gt;Past&lt;/h2&gt;
&lt;p&gt;Growing up I played tennis. I was good, competed at province level. I disliked training, but I loved competing.&lt;/p&gt;
&lt;p&gt;When it got to either train daily or stop competing at high level; I stopped.&lt;/p&gt;
&lt;p&gt;Since then I barely did any sport.&lt;/p&gt;
&lt;p&gt;At 23 I started boxing. At 24 I started running consistently, boxing daily and going to the gym with a purpose.&lt;/p&gt;
&lt;p&gt;In my first boxing fight I realized I’d lost my competitiveness. It felt bad. I was unsure if it would come back.&lt;/p&gt;
&lt;h2 id=&quot;present&quot;&gt;Present&lt;/h2&gt;
&lt;p&gt;I hate running. I hate lifting. I dislike getting punched in the face.&lt;/p&gt;
&lt;p&gt;I enjoy pain. It makes me feel alive, brings me down to earth.&lt;/p&gt;
&lt;p&gt;I’ve mechanized my day: Wake up -&gt; box -&gt; work -&gt; run -&gt; finish work -&gt; gym/run -&gt; rest. I’ve gradually built up to it, and most weeks I end up exhausted. &lt;br&gt;
The hardest part is starting. It’s easy to find excuses.&lt;/p&gt;
&lt;p&gt;I have so much curiosity to find out how far I can push my body.&lt;/p&gt;
&lt;p&gt;It’s very rewarding to see progress. In running and lifting it’s easy to measure.&lt;/p&gt;
&lt;p&gt;I recently did another fight, and I had my competitiveness back.&lt;/p&gt;
&lt;p&gt;My physique is improving. It makes me feel good.&lt;/p&gt;
&lt;h2 id=&quot;future&quot;&gt;Future&lt;/h2&gt;
&lt;p&gt;I don’t know the form my schedule may have, but I will keep growing.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>WebMCP</title>
    <link>https://miquelpuigturon.com/meta/webmcp/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/meta/webmcp/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>WebMCP This site is machine-friendly: every page doubles as a WebMCP server — a proposed W3C standard that lets AI agents running in your browser discover and use tools exposed by the page you’re…</description>
    <content:encoded>&lt;h1 id=&quot;webmcp&quot;&gt;WebMCP&lt;/h1&gt;
&lt;p&gt;This site is machine-friendly: every page doubles as a &lt;a href=&quot;https://github.com/webmachinelearning/webmcp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;WebMCP&lt;/a&gt; server — a proposed W3C standard that lets &lt;strong&gt;AI agents running in your browser&lt;/strong&gt; discover and use tools exposed by the page you’re visiting.&lt;/p&gt;
&lt;section class=&quot;agent-tools&quot; data-astro-cid-ikzq4idk&gt; &lt;div class=&quot;agent-status&quot; id=&quot;webmcp-status&quot; data-astro-cid-ikzq4idk&gt;WebMCP: checking…&lt;/div&gt; &lt;div class=&quot;agent-tools-list&quot; data-astro-cid-ikzq4idk&gt; &lt;h3 data-astro-cid-ikzq4idk&gt;Tools available to browser agents&lt;/h3&gt; &lt;ul data-astro-cid-ikzq4idk&gt; &lt;li data-astro-cid-ikzq4idk&gt;&lt;code data-astro-cid-ikzq4idk&gt;search_site&lt;/code&gt; &lt;span class=&quot;tag read&quot; data-astro-cid-ikzq4idk&gt;read&lt;/span&gt; — semantic + keyword search over every page of this site.&lt;/li&gt; &lt;li data-astro-cid-ikzq4idk&gt;&lt;code data-astro-cid-ikzq4idk&gt;read_page&lt;/code&gt; &lt;span class=&quot;tag read&quot; data-astro-cid-ikzq4idk&gt;read&lt;/span&gt; — full text of any page, by path.&lt;/li&gt; &lt;li data-astro-cid-ikzq4idk&gt;&lt;code data-astro-cid-ikzq4idk&gt;ask_site&lt;/code&gt; &lt;span class=&quot;tag read&quot; data-astro-cid-ikzq4idk&gt;read&lt;/span&gt; — ask a question; get a generated answer grounded in this site, with sources.&lt;/li&gt; &lt;li data-astro-cid-ikzq4idk&gt;&lt;code data-astro-cid-ikzq4idk&gt;get_site_map&lt;/code&gt; &lt;span class=&quot;tag read&quot; data-astro-cid-ikzq4idk&gt;read&lt;/span&gt; — list of all page paths.&lt;/li&gt; &lt;li data-astro-cid-ikzq4idk&gt;&lt;code data-astro-cid-ikzq4idk&gt;contact_miquel&lt;/code&gt; &lt;span class=&quot;tag write&quot; data-astro-cid-ikzq4idk&gt;write&lt;/span&gt; — send the site owner a message on the user&apos;s behalf (agents confirm with the user first).&lt;/li&gt; &lt;/ul&gt; &lt;p class=&quot;agent-note&quot; data-astro-cid-ikzq4idk&gt;
These tools are registered on &lt;strong data-astro-cid-ikzq4idk&gt;every page&lt;/strong&gt; of this site, not just this
      one — this page exists to show their status and document them. They follow the
&lt;a href=&quot;https://github.com/webmachinelearning/webmcp&quot; data-astro-cid-ikzq4idk&gt;WebMCP proposal&lt;/a&gt; (Chrome
      origin trial — enable &lt;code data-astro-cid-ikzq4idk&gt;chrome://flags/#enable-webmcp-testing&lt;/code&gt;, or the site
      needs an origin-trial token).
&lt;/p&gt; &lt;p class=&quot;agent-note&quot; data-astro-cid-ikzq4idk&gt;
Agents without browser WebMCP can use the hosted MCP endpoint at
&lt;code data-astro-cid-ikzq4idk&gt;https://api.miquelpuigturon.com/mcp&lt;/code&gt;.
&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; &lt;script data-astro-rerun&gt;
  (function () {
    var statusEl = document.getElementById(&apos;webmcp-status&apos;);

    function setStatus(text, ok) {
      if (!statusEl) return;
      statusEl.textContent = text;
      statusEl.classList.toggle(&apos;ok&apos;, ok === true);
      statusEl.classList.toggle(&apos;inactive&apos;, ok === false);
    }

    var mc = navigator.modelContext || document.modelContext;
    if (!mc || typeof mc.getTools !== &apos;function&apos;) {
      setStatus(
        &apos;WebMCP: not active — this browser does not expose modelContext. Try Chrome with chrome://flags/#enable-webmcp-testing.&apos;,
        false,
      );
      return;
    }

    var tries = 0;
    function check() {
      mc.getTools().then(function (tools) {
        if (tools.length &gt; 0) {
          setStatus(&apos;WebMCP: active — &apos; + tools.length + &apos; tools registered for browser agents.&apos;, true);
        } else if (tries++ &lt; 20) {
          setTimeout(check, 100);
        } else {
          setStatus(&apos;WebMCP: available, but no tools were registered.&apos;, false);
        }
      });
    }
    check();
  })();
&lt;/script&gt; 
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;
&lt;p&gt;When a WebMCP-capable agent visits any page on this site, the tools listed above are registered through the browser’s &lt;code&gt;modelContext&lt;/code&gt; API. The agent can then search everything I’ve written, read any page, ask questions grounded in the site’s content — or send me a message on your behalf (it will ask you for your name, email and message first; that tool is marked as a write, so agents confirm before using it). All of this runs without any server-side integration, using your own browsing session.&lt;/p&gt;
&lt;p&gt;On this site, the tools are backed by &lt;a href=&quot;https://developers.cloudflare.com/ai-search/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare AI Search&lt;/a&gt;, which crawls and indexes all pages (synced daily).&lt;/p&gt;
&lt;h2 id=&quot;for-agents-without-browser-webmcp&quot;&gt;For agents without browser WebMCP&lt;/h2&gt;
&lt;p&gt;The same search capability is exposed as a hosted &lt;a href=&quot;https://modelcontext.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Model Context Protocol&lt;/a&gt; endpoint on this site’s own API domain, plus a small public HTTP API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP endpoint&lt;/strong&gt;: &lt;code&gt;https://api.miquelpuigturon.com/mcp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search API&lt;/strong&gt;: &lt;code&gt;POST https://78a22e35-6a11-4b9a-84fe-1ee03a805a39.search.ai.cloudflare.com/search&lt;/code&gt; with &lt;code&gt;{&amp;quot;query&amp;quot;: &amp;quot;...&amp;quot;}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ask API&lt;/strong&gt; (OpenAI-compatible): &lt;code&gt;POST .../chat/completions&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;status&quot;&gt;Status&lt;/h2&gt;
&lt;p&gt;WebMCP is experimental: it is shipping as a Chrome origin trial. Until it lands, the tools activate only in browsers that enable it (e.g. via &lt;code&gt;chrome://flags/#enable-webmcp-testing&lt;/code&gt;). The hosted MCP endpoint above works everywhere, today.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Pain, Suffering and God</title>
    <link>https://miquelpuigturon.com/self/pain-suffering-and-god/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/self/pain-suffering-and-god/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Pain, Suffering and God I always knew I wanted to build something mine. I did not expect the amount of pain and suffering that would come with taking the leap of faith. I now believe that pain and…</description>
    <content:encoded>&lt;h1 id=&quot;pain-suffering-and-god&quot;&gt;Pain, Suffering and God&lt;/h1&gt;
&lt;p&gt;I always knew I wanted to build something mine.&lt;/p&gt;
&lt;p&gt;I did not expect the amount of pain and suffering that would come with taking the leap of faith.&lt;/p&gt;
&lt;p&gt;I now believe that pain and suffering are warranted.&lt;br&gt;
And that success is a gift from God.&lt;/p&gt;
&lt;p&gt;I see myself as a pirate searching for a treasure, sailing a violent sea. Every now and then, a new storm to sail through.&lt;/p&gt;
&lt;h2 id=&quot;pain&quot;&gt;Pain&lt;/h2&gt;
&lt;p&gt;Nothing is given. Every little thing requires effort, and most effort goes unrewarded.&lt;br&gt;
At first it’s annoying, then angering, at last depressing.&lt;/p&gt;
&lt;p&gt;Pain is the only guarantee.&lt;/p&gt;
&lt;h2 id=&quot;suffering&quot;&gt;Suffering&lt;/h2&gt;
&lt;p&gt;Pain is unavoidable. You choose how much to suffer.&lt;/p&gt;
&lt;p&gt;The crew is very important, shared pain leads to laughter.&lt;br&gt;
When you decide it’s okay to sink and there’s someone that refuses to, you live until the next storm.&lt;br&gt;
The wrong crew leads to the most suffering.&lt;/p&gt;
&lt;h2 id=&quot;god&quot;&gt;God&lt;/h2&gt;
&lt;p&gt;Finding the treasure is mostly luck. When I think about it I see God. As faceless as I am faithless.&lt;/p&gt;
&lt;p&gt;I can curse him, thank him and everything in between, it only changes my mood, which is enough.&lt;/p&gt;
&lt;h2 id=&quot;all-in-all&quot;&gt;All in all&lt;/h2&gt;
&lt;p&gt;The world is not made for people who want to be different. They are rewarded with pain and opt into suffering beyond what they imagined.&lt;br&gt;
It’s ok, brings them closer to God.&lt;/p&gt;
&lt;p&gt;Failure is a choice, success is a gift from God. You can always choose pain.&lt;/p&gt;
&lt;p&gt;I have had my ups-and-downs. I’ve wanted to quit: throw the boat, stay on land.&lt;br&gt;
I’ve wished not to find the map so I could stop searching for the treasure.&lt;br&gt;
But God had other plans, picked me up when I was at my lowest.&lt;/p&gt;
&lt;p&gt;I want to smile to the pain, sail the seven seas, and if I have to fight to stay afloat, so be it.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>OOM investigation</title>
    <link>https://miquelpuigturon.com/humanity/temporal/oom-investigation/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/temporal/oom-investigation/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>OOM investigation A production debugging story: from “everything is crashing” to finding a 1-line root cause buried under 12MB of tokenizer data. The Setup We run a Temporal TypeScript worker that…</description>
    <content:encoded>&lt;h1 id=&quot;oom-investigation&quot;&gt;OOM investigation&lt;/h1&gt;
&lt;p&gt;A production debugging story: from “everything is crashing” to finding a 1-line root cause buried under 12MB of tokenizer data.&lt;/p&gt;
&lt;h2 id=&quot;the-setup&quot;&gt;The Setup&lt;/h2&gt;
&lt;p&gt;We run a Temporal TypeScript worker that manages long-running conversation workflows. Each workflow orchestrates multi-channel interactions with leads. The worker runs on ECS with 4GB task memory (~3.8GB for the container), configured with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;workflowThreadPoolSize: 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxCachedWorkflows: 300&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;workflowTaskSlotSupplier: { numSlots: 12 }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reuseV8Context: true&lt;/code&gt; (default)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--max-old-space-size=3072&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This configuration had been stable for months. It is explained in &lt;a href=&quot;/humanity/temporal/worker-configuration/&quot;&gt;worker configuration&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;February 6, 11am. After a routine deployment, our conversation worker instances started dying in a loop. ECS kept restarting them, they’d run for about 60 seconds, then get OOM-killed. Every single instance, every single time.&lt;/p&gt;
&lt;p&gt;The deployment included ~25 commits: an AI SDK v5-to-v6 migration, a URL parsing refactor, a new “previously contacted” substatus, task management changes, and various smaller fixes. Nothing that screamed “memory bomb.”&lt;/p&gt;
&lt;h2 id=&quot;phase-1-rule-out-the-obvious&quot;&gt;Phase 1: Rule Out the Obvious&lt;/h2&gt;
&lt;h3 id=&quot;hypothesis-outlier-campaign-graph-loop&quot;&gt;Hypothesis: Outlier Campaign Graph Loop&lt;/h3&gt;
&lt;p&gt;This was the first and longest part of the investigation, because we’d been bitten by this exact pattern before.&lt;/p&gt;
&lt;p&gt;Each campaign in our system is modeled as a directed graph - nodes are sequence steps (send email, wait, check reply, branch on condition, etc.) and edges define the flow between them. A conversation workflow walks this graph as leads progress through the campaign. If the graph contains a cycle - a loop back to an earlier node - building the graph can cause infinite recursion. We’d had a previous incident where exactly this happened: a malformed campaign graph caused workflows to loop, their in-memory state ballooned, and the workers OOM’d.&lt;/p&gt;
&lt;p&gt;So when the same symptoms appeared, this was the natural suspect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tests:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queried all campaigns active around the incident window (~11am Feb 6), looking for recently created or modified campaigns that could have introduced a loop.&lt;/li&gt;
&lt;li&gt;Inspected campaign graphs for cycles, focusing on campaigns with high conversation counts.&lt;/li&gt;
&lt;li&gt;Measured individual conversation workflow state and sequence payload sizes to look for the telltale ballooning pattern (a looping workflow accumulates megabytes of step history).&lt;/li&gt;
&lt;li&gt;Tested specific suspected campaign IDs by isolating them - checking whether stopping certain campaigns would stabilize the workers.&lt;/li&gt;
&lt;li&gt;Filtered to active-only conversation subsets to narrow the search.&lt;/li&gt;
&lt;li&gt;Cross-referenced campaign &lt;code&gt;launchedAt&lt;/code&gt; / &lt;code&gt;updatedAt&lt;/code&gt; timestamps with the exact incident start time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; No outlier. Conversation state sizes were normal (not the multi-MB explosion you’d see from a loop). No campaign graph had an obvious cycle. No single campaign’s removal stabilized things. The pattern didn’t fit: a graph loop causes one or a few workflows to explode, but here &lt;em&gt;every&lt;/em&gt; instance was dying regardless of which workflows they happened to cache. This pointed away from a campaign-specific issue toward something systemic.&lt;/p&gt;
&lt;p&gt;Still, this phase consumed significant time because the previous loop incident had been so similar in symptoms. Too much time was spent here.&lt;/p&gt;
&lt;h3 id=&quot;hypothesis-retry-storm--stuck-activities&quot;&gt;Hypothesis: Retry Storm / Stuck Activities&lt;/h3&gt;
&lt;p&gt;Maybe a subset of workflows or activities were retrying aggressively and driving memory + CPU.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tests:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temporal CLI checks on running workflows and pending activity attempts.&lt;/li&gt;
&lt;li&gt;Focus on &lt;code&gt;conversationWorkflow&lt;/code&gt; and &lt;code&gt;conversationCallManagerWorkflow&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Added interceptor logging at activity start to capture which activities were firing, their &lt;code&gt;workflowId&lt;/code&gt;, and retry attempt number - this let us see if any activity was retrying excessively across the fleet.&lt;/li&gt;
&lt;li&gt;Added workflow replay logging to detect if workflows were being replayed repeatedly (which would indicate nondeterminism errors or sticky cache eviction churn).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Some noisy workflows existed, but no single smoking gun retry loop explaining cluster-wide OOM. A known nondeterminism error existed in one workflow, but a single errored workflow doesn’t explain fleet-wide memory growth. The interceptor logs showed normal activity distribution - no hot retriers dominating the fleet. Ruled out.&lt;/p&gt;
&lt;h3 id=&quot;hypothesis-nondeterminism-error&quot;&gt;Hypothesis: Nondeterminism Error&lt;/h3&gt;
&lt;p&gt;The deployment included a refactor of &lt;code&gt;getPublicIdentifierFromUrl&lt;/code&gt; that changed URL parsing behavior. In Temporal, changing the behavior of functions used in workflow code breaks replay determinism. We found 46 conversations with protocol-less URLs that would hit this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; Reverted the change and deployed.
&lt;strong&gt;Result:&lt;/strong&gt; Still crashing. Ruled out.&lt;/p&gt;
&lt;h3 id=&quot;hypothesis-ai-sdk-v6-memory-increase&quot;&gt;Hypothesis: AI SDK v6 Memory Increase&lt;/h3&gt;
&lt;p&gt;The AI SDK upgrade was the biggest change. We measured heap usage by importing the old vs new packages in isolation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; Compared &lt;code&gt;process.memoryUsage().heapUsed&lt;/code&gt; with AI SDK v5 vs v6.
&lt;strong&gt;Result:&lt;/strong&gt; +5.3MB heap difference. Not enough to explain multi-GB growth. Ruled out as sole cause.&lt;/p&gt;
&lt;h3 id=&quot;hypothesis-nodejs-version-change&quot;&gt;Hypothesis: Node.js Version Change&lt;/h3&gt;
&lt;p&gt;Our Dockerfile used &lt;code&gt;node:22-bookworm-slim&lt;/code&gt; (a moving tag). It had silently updated from 22.21.1 to 22.22.0 on Feb 3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; Pinned to &lt;code&gt;FROM node:22.21.1-bookworm-slim&lt;/code&gt; and deployed.
&lt;strong&gt;Result:&lt;/strong&gt; Still crashing. Ruled out.&lt;/p&gt;
&lt;h2 id=&quot;phase-2-observing-the-crash-pattern&quot;&gt;Phase 2: Observing the Crash Pattern&lt;/h2&gt;
&lt;p&gt;At this point, every obvious hypothesis had been eliminated. We stepped back and looked at the external monitoring data.&lt;/p&gt;
&lt;h3 id=&quot;sticky-cache-sawtooth&quot;&gt;Sticky Cache Sawtooth&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/humanity/temporal/workflow-bundle/temporal-sticky-cache-size.png&quot; alt=&quot;temporal sticky cache size&quot; style=&quot;max-width: 100%;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;temporal_sticky_cache_size&lt;/code&gt; metric showed a repeating sawtooth pattern. Workers would start, the sticky cache would fill as workflows got assigned, and then the instance would die. Restart, fill, die. Over and over.&lt;/p&gt;
&lt;h3 id=&quot;ecs-memory-utilization&quot;&gt;ECS Memory Utilization&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/humanity/temporal/workflow-bundle/ecs-memory-utilization.png&quot; alt=&quot;ecs memory utilization&quot; style=&quot;max-width: 100%;&quot;&gt;&lt;/p&gt;
&lt;p&gt;ECS MemoryUtilization told the same story from the infrastructure side. Both average (blue) and maximum (orange) climbed, then dropped sharply as containers were OOM-killed and restarted.&lt;/p&gt;
&lt;p&gt;The correlation was clear: &lt;strong&gt;memory grew in lockstep with the number of cached workflows.&lt;/strong&gt; Each workflow the cache absorbed pushed the container closer to its limit. This shifted our thinking from “some bad workflow is exploding” to “every cached workflow costs more memory than it should.”&lt;/p&gt;
&lt;h2 id=&quot;phase-3-adding-diagnostics&quot;&gt;Phase 3: Adding Diagnostics&lt;/h2&gt;
&lt;p&gt;We were still flying blind on &lt;em&gt;why&lt;/em&gt; per-workflow cost had changed. &lt;code&gt;process.memoryUsage()&lt;/code&gt; only tells you about the main thread’s V8 heap. We needed visibility into RSS (total process memory) to see what’s happening outside the heap.&lt;/p&gt;
&lt;p&gt;We added a 10-second interval logger:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setInterval&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; mem&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; process.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;memoryUsage&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; heapUsedMb&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;round&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(mem.heapUsed &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; rssMb&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;round&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(mem.rss &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; externalMb&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;round&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(mem.external &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; arrayBuffersMb&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;round&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(mem.arrayBuffers &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; estimatedNativeMb&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;max&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, rssMb &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; heapUsedMb &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; externalMb &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; arrayBuffersMb)&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;color:#E1E4E8&quot;&gt;  console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Worker diagnostics&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    workerState: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getState&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;?.(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    workerStatus: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getStatus&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;?.(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    heapUsedMB: heapUsedMb,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    rssMB: rssMb,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    estimatedNativeMB: estimatedNativeMb,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    rssDeltaMB: rssMb &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; previousRssMb,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    heapUsedDeltaMB: heapUsedMb &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; previousHeapUsedMb,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10_000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;workerStatus&lt;/code&gt; was key - it gives &lt;code&gt;numCachedWorkflows&lt;/code&gt;, which let us correlate memory with cache size numerically, not just visually.&lt;/p&gt;
&lt;h3 id=&quot;first-diagnostic-results-12-workflow-task-slots&quot;&gt;First Diagnostic Results (12 workflow task slots)&lt;/h3&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;Cached WFs&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Heap (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;RSS (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Native (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Delta/10s (MB)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;~20&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;280&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;1,389&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~1,100&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;~80&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;300&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;2,950&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~2,650&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+780&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;~100&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;314&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;3,697&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~3,383&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+374&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;&lt;em&gt;killed&lt;/em&gt;&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two immediate observations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Heap was fine.&lt;/strong&gt; 280-314 MB, stable. The V8 garbage collector was doing its job.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RSS was exploding.&lt;/strong&gt; Growing by 400-800 MB every 10 seconds. All outside the heap.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The gap between RSS and heap (what we called “native” memory) was the problem. This includes the Temporal Rust Core bridge, V8 worker thread heaps, and compiled code. But 3.3 GB of “native” memory was absurd.&lt;/p&gt;
&lt;h2 id=&quot;phase-4-reducing-configuration&quot;&gt;Phase 4: Reducing Configuration&lt;/h2&gt;
&lt;h3 id=&quot;hypothesis-too-many-concurrent-workflow-tasks&quot;&gt;Hypothesis: Too Many Concurrent Workflow Tasks&lt;/h3&gt;
&lt;p&gt;Maybe 12 concurrent workflow slots created too many V8 isolates at once.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; Reduced &lt;code&gt;workflowTaskSlotSupplier.numSlots&lt;/code&gt; from 12 to 6.
&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;Cached WFs&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Heap (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;RSS (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Native (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Delta/10s (MB)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;61&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;247&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;1,597&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;1,320&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+434&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;111&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;230&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;2,176&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;1,922&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+579&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;124&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;243&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;3,200&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;2,931&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+1,024&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;&lt;em&gt;killed&lt;/em&gt;&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Still crashing. Slower, but same pattern. The cache fills to 124 in ~40 seconds, RSS hits 3.2 GB, OOM. &lt;br&gt;
In hindsight this was obviously not the issue since V8 isolates are reused.&lt;/p&gt;
&lt;h3 id=&quot;what-the-data-told-us&quot;&gt;What the Data Told Us&lt;/h3&gt;
&lt;p&gt;From snapshots 1 to 2: 50 more cached workflows cost +602 MB of native memory. That’s &lt;strong&gt;~12 MB per cached workflow&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;reuseV8Context: true&lt;/code&gt;, the Temporal SDK shares a single V8 context across all workflows per thread. Per-workflow memory should be ~1 MB (just the workflow’s local state). 12 MB was 12x too high.&lt;/p&gt;
&lt;h3 id=&quot;cross-reference-the-enrich-service&quot;&gt;Cross-Reference: The Enrich Service&lt;/h3&gt;
&lt;p&gt;Our other Temporal worker (enrich service) uses the same &lt;code&gt;@enginy/common&lt;/code&gt; package, the same AI SDK v6, and has &lt;code&gt;maxCachedWorkflows: 350&lt;/code&gt; with &lt;em&gt;less&lt;/em&gt; container memory (3.5 GB). It wasn’t crashing.&lt;/p&gt;
&lt;p&gt;This told us the problem was specific to what’s inside the conversation workflow bundle, not the shared infrastructure.&lt;/p&gt;
&lt;h2 id=&quot;phase-5-finding-the-root-cause&quot;&gt;Phase 5: Finding the Root Cause&lt;/h2&gt;
&lt;p&gt;With the per-workflow cost established at ~12 MB and the enrich service working fine, we turned to the workflow bundle itself.&lt;/p&gt;
&lt;p&gt;Temporal workflow code doesn’t run directly from your source files. Instead, the Temporal TypeScript SDK uses webpack to produce a self-contained &lt;code&gt;workflow-bundle.js&lt;/code&gt; (or you can generate it at build time like we do). This bundle is what gets loaded into V8 worker thread isolates.&lt;/p&gt;
&lt;p&gt;The reason for this separate bundling step is determinism: the SDK needs to control the execution environment inside the isolate, patching non-deterministic APIs like &lt;code&gt;Date()&lt;/code&gt; and &lt;code&gt;Math.random()&lt;/code&gt; with deterministic replacements so that workflow replay produces identical results. Activities run on the main thread and can import anything normally, but workflow code can only access what’s in the bundle.&lt;/p&gt;
&lt;p&gt;This means whatever ends up in the bundle gets loaded into every V8 worker context. And anything that shouldn’t be there - that wasn’t there before - directly inflates per-workflow memory cost.&lt;/p&gt;
&lt;p&gt;What was in the conversation bundle that wasn’t in the enrich bundle?&lt;/p&gt;
&lt;p&gt;We grepped the built &lt;code&gt;workflow-bundle.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; 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;color:#B392F0&quot;&gt;grep&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;gpt-tokenizer\|creditsCostCalculator\|encodeChat&apos;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; conversation/dist/workflow-bundle.js&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result was damning. The entire &lt;code&gt;gpt-tokenizer&lt;/code&gt; package - including its ~2 MB BPE rank tables with 200,000 tokenizer merge rules - was embedded in the workflow bundle. This data gets decoded into ~20 MB+ of in-memory JavaScript objects when the V8 context evaluates the bundle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before the fix:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bundle size: &lt;strong&gt;~12 MB&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Heavy-module hits in bundle: &lt;strong&gt;96&lt;/strong&gt; (&lt;code&gt;ai-sdk&lt;/code&gt; / tokenizer related)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-import-chain&quot;&gt;The Import Chain&lt;/h3&gt;
&lt;p&gt;The workflow code legitimately imports utilities from &lt;code&gt;@enginy/common/browser&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// conversation/src/workflows/utils/conversationIdentityActionsReceiver.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { getMillisecondsToNextWorkingHour, isInWorkingHours } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;@enginy/common/browser&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;browser.ts&lt;/code&gt; is a barrel file with ~130 exports. A recent commit (&lt;code&gt;8ecd24ed9&lt;/code&gt;, Feb 5 - “AI Variable Credits Not Showing Full Cycle Cost”) had added one line:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { getPromptCreditCost } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;./credits/creditsCostCalculator&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;creditsCostCalculator.ts&lt;/code&gt; imports &lt;code&gt;encodeChat&lt;/code&gt; from &lt;code&gt;gpt-tokenizer&lt;/code&gt;. No workflow code uses &lt;code&gt;getPromptCreditCost&lt;/code&gt;. Webpack should have tree-shaken it out.&lt;/p&gt;
&lt;h3 id=&quot;why-tree-shaking-failed&quot;&gt;Why Tree-Shaking Failed&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; 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;color:#B392F0&quot;&gt;grep&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;sideEffects&apos;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; common/package.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# (empty)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@enginy/common&lt;/code&gt;’s &lt;code&gt;package.json&lt;/code&gt; had no &lt;code&gt;sideEffects&lt;/code&gt; field. Without &lt;code&gt;&quot;sideEffects&quot;: false&lt;/code&gt;, webpack must assume that importing &lt;em&gt;any&lt;/em&gt; export from a module might trigger side effects. It can’t safely remove &lt;code&gt;getPromptCreditCost&lt;/code&gt; because the act of importing &lt;code&gt;creditsCostCalculator.ts&lt;/code&gt; might do something important (register a global, modify a prototype, etc.).&lt;/p&gt;
&lt;p&gt;So webpack included &lt;code&gt;creditsCostCalculator.ts&lt;/code&gt;, which pulled in &lt;code&gt;gpt-tokenizer&lt;/code&gt;, which pulled in its BPE rank tables, encoding params, model maps, and everything else.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;Added one field to &lt;code&gt;common/package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;sideEffects&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells webpack: “every module in this package is pure - if you don’t use an export, you can safely remove it.” Webpack then tree-shakes &lt;code&gt;getPromptCreditCost&lt;/code&gt; and its entire dependency chain (&lt;code&gt;gpt-tokenizer&lt;/code&gt;) out of the workflow bundle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After rebuild:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bundle size: &lt;strong&gt;~2.9 MB&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Heavy-module hits: &lt;strong&gt;0&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;after-the-fix-runtime&quot;&gt;After the Fix (Runtime)&lt;/h3&gt;















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;Cached WFs&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Heap (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;RSS (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Native (MB)&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Delta/10s (MB)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;0&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;195&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;501&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;285&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;4&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;192&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;535&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;322&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+34&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;29&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;199&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;543&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;322&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+5&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;78&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;214&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;638&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;386&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;92&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;189&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;593&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;376&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;+1&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;92 cached workflows at 593 MB RSS. Before the fix, 61 cached workflows was already 1,597 MB. Per-workflow cost dropped from ~12 MB back to ~1 MB.&lt;/p&gt;
&lt;p&gt;The base RSS dropped from ~1,200 MB to ~500 MB - the gpt-tokenizer tables were consuming ~700 MB just being loaded into the two V8 worker thread contexts.&lt;/p&gt;
&lt;h2 id=&quot;lessons&quot;&gt;Lessons&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sideEffects: false&lt;/code&gt; matters.&lt;/strong&gt; If you maintain a shared library consumed by webpack, declare it. Without it, a single barrel export can drag megabytes of unrelated code into your bundle. In our case, a tokenizer that no workflow ever calls.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Barrel exports are a liability in bundled contexts.&lt;/strong&gt; Our &lt;code&gt;browser.ts&lt;/code&gt; has 130+ exports. Any one of them can pull in unexpected transitive dependencies. For performance-critical bundles like Temporal workflow bundles, consider importing from specific subpaths instead of barrels.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;process.memoryUsage().heapUsed&lt;/code&gt; only covers the main thread.&lt;/strong&gt; In Temporal workers, activities run on the main thread while workflows run in separate V8 worker threads. If heap is stable but RSS climbs, that tells you the problem lives in the workflow threads (bundle, V8 worker contexts), not in activity code. It narrows &lt;em&gt;where&lt;/em&gt; to look, but the actual root cause still requires inspecting the bundle and correlating memory with workload counters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Temporal &lt;code&gt;workerStatus&lt;/code&gt; object is invaluable.&lt;/strong&gt; &lt;code&gt;numCachedWorkflows&lt;/code&gt; let us correlate memory growth with cache size, which pointed us toward per-workflow cost as the problem.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inspect built artifacts, not only source.&lt;/strong&gt; The bug was invisible in the source code - no workflow imports &lt;code&gt;gpt-tokenizer&lt;/code&gt;. Only by grepping the built &lt;code&gt;workflow-bundle.js&lt;/code&gt; did we find the 12 MB of dead weight. When memory regressions appear after a deploy, validate what actually lands in the bundle.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The investigation loop works.&lt;/strong&gt; Every phase followed the same pattern: form a hypothesis based on available data, design a minimal test, deploy, observe. Most hypotheses were wrong. That’s fine - each one narrowed the search space.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;
&lt;p&gt;After fixing the conversation worker, I investigated the same class of problem in our enrich service. That one had no single-line fix - it turned into a bundle experiment log. You can read about it in the &lt;a href=&quot;/humanity/temporal/oom-investigation/deep-dive/&quot;&gt;enrich deep dive&lt;/a&gt;.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Worker configuration (Node)</title>
    <link>https://miquelpuigturon.com/humanity/temporal/worker-configuration/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/temporal/worker-configuration/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Worker configuration ( Node ) This is a walkthrough of my experience configuring workers running on Node for Genesy . It focuses on worker configuration, as I found the current (as of Oct 2025)…</description>
    <content:encoded>&lt;h1 id=&quot;worker-configuration-node&quot;&gt;Worker configuration (&lt;em&gt;Node&lt;/em&gt;)&lt;/h1&gt;
&lt;p&gt;This is a walkthrough of my experience configuring workers running on Node for &lt;a href=&quot;https://genesy.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Genesy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It focuses on worker configuration, as I found the current (as of Oct 2025) defaults to be misguiding, and want to share my findings, as this is &lt;a href=&quot;https://temporalio.slack.com/archives/CTRCR8RBP/p1761316809891479&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;not only a me-issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Readers should understand workers and worker/server interaction before reading this article. (&lt;a href=&quot;https://temporal.io/blog/building-reliable-distributed-systems-in-node-js-part-2&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a good resource&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Fun fact&lt;/em&gt;: This article is the reason this site exists.&lt;/p&gt;
&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;
&lt;details style=&quot;padding: 1rem;&quot;&gt;&lt;summary style=&quot;cursor: pointer; margin-bottom: 1rem;&quot;&gt;Click to expand&lt;/summary&gt;&lt;p&gt;We run two &lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; services  &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;Each service simply runs a worker&lt;/span&gt; &lt;/span&gt; &lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Enrich&lt;/strong&gt;: User actions are not processed in the API directly, they are off-loaded to this service, which makes sure that they are processed reliably. I wrote my &lt;a href=&quot;https://upcommons.upc.edu/entities/publication/d8662ade-c751-4257-bc1f-fb2f8245199d&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CS bachelor’s thesis&lt;/a&gt; on why we decided on this, and how I coded the primitives that allow it to work well.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conversation&lt;/strong&gt;: Genesy offers &lt;a href=&quot;https://www.genesy.ai/#:~:text=Conditional%20Campaigns&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;automated campaigns&lt;/a&gt;, which are likely the most advanced conditional campaign system that exist on any tool. Each campaign has an identity which targets contacts, with which a conversation is started. Each conversation runs as an entity workflow, and identity actions are centralized in a manager workflow.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Even though the learnings apply to both services, most of the investigation comes from the &lt;strong&gt;Conversation&lt;/strong&gt; service, as we want it to scale to infinity reliably (started in February with &amp;lt;50k active conversations, in October it handles &amp;gt;500k).&lt;/p&gt;&lt;h3 id=&quot;why-did-i-start-looking-into-this&quot;&gt;Why did I start looking into this&lt;/h3&gt;&lt;p&gt;Users can see conversations in Genesy via the &lt;em&gt;inbox&lt;/em&gt;. The inbox also allows sending messages directly.&lt;/p&gt;&lt;p&gt;There’s nothing worse for users than sending a message twice. To avoid this, we use &lt;a href=&quot;https://docs.temporal.io/encyclopedia/workflow-message-passing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;updates&lt;/a&gt; to prompt the manager workflow to send the message, and get a response (whether it was successful or not).&lt;/p&gt;&lt;p&gt;If the manager workflow is not responsive, then the update fails, and the user is sad. As my main job is to make users happy, this had to be solved.&lt;/p&gt;&lt;h3 id=&quot;priority&quot;&gt;Priority&lt;/h3&gt;&lt;p&gt;&lt;em&gt;6th of march&lt;/em&gt;, Temporal &lt;a href=&quot;https://github.com/temporalio/temporal/issues/1507#issuecomment-2703831058&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;announced&lt;/a&gt; priority and fairness.&lt;/p&gt;&lt;p&gt;It took 3 months of &lt;a href=&quot;https://temporalio.slack.com/archives/C08GSTH8WEM/p1758618625126139&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;me complaining&lt;/a&gt; that it wasn’t working, until priority was an actual useful feature.&lt;/p&gt;&lt;p&gt;I thought that priority would solve this problem. Simply prioritize manager workflows over conversation workflows.
Then we only need to make sure that we can handle &lt;em&gt;hundreds&lt;/em&gt; of workflows immediately instead of &lt;em&gt;hundreds of thousands&lt;/em&gt;.&lt;/p&gt;&lt;/details&gt;
&lt;h2 id=&quot;discovering-the-problem&quot;&gt;Discovering the problem&lt;/h2&gt;
&lt;p&gt;I started experimenting with how to make sure &lt;code&gt;priorityKey=1&lt;/code&gt; workflows are always responsive no matter how many higher priority workflows needed processing. An easy way to stress test it was to limit the number of instances of the service, and thus of workers.&lt;/p&gt;
&lt;p&gt;Previously, up to 50 instances were running at a time, as our best guess on making &lt;code&gt;priorityKey=1&lt;/code&gt; workflows responsive was to process everything instantly. &lt;br/&gt;
I cut down to 10 instances, and at peak times, workers were &lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; saturated &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;By saturated I mean that for the task queue, the backlog increases: tasksAddRate &amp;gt; tasksDispatchRate&lt;/span&gt; &lt;/span&gt; .&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0; text-align: center;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/stats-by-prioQ.png&quot; alt=&quot;stats by prioQ&quot; style=&quot;max-width: 50%;&quot;/&gt;&lt;figcaption style=&quot;font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Stats for the conversation task queue by priority key&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Even though it looks great, as the backlog age is less than one second for &lt;code&gt;priorityKey=1&lt;/code&gt;, it performed worse. &lt;br/&gt;
That day, we went from ~97% success on users sending messages to ~85%.&lt;/p&gt;
&lt;p&gt;At this point, tasks were picked up by workers almost instantly, yet they failed. Why?? &lt;br/&gt;
Looking at metrics, many workflow tasks were failing due to &lt;code&gt;status_code=not_found&lt;/code&gt;&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/failures-workflowtasklatency.png&quot; alt=&quot;workflow task failures and latency&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Workflow task failures and latency&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;It’s obvious from the image that the failures are related to the &lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; task execution latency &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;Time since the worker picks up the task until it responds to the temporal server&lt;/span&gt; &lt;/span&gt; . &lt;em&gt;kappa&lt;/em&gt; agreed:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;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;metric temporal_request_failure with operation=RespondWorkflowTaskCompleted and&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;status_code=NOT_FOUND indicates that a worker attempted to report the completion&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;of a workflow task, but the Temporal service could not find the corresponding&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;workflow task. This typically happens when the workflow task has already timed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;out—meaning the worker did not respond within the default 10-second timeout window&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The worker was taking more than 10 seconds (default start-to-close timeout) to complete the task. &lt;br/&gt;
This was a surprise, as workflow tasks are lightweight, and should take &lt;em&gt;tens of milliseconds&lt;/em&gt; at most.&lt;/p&gt;
&lt;h2 id=&quot;understanding-worker-configuration&quot;&gt;Understanding worker configuration&lt;/h2&gt;
&lt;p&gt;The worker configuration is defined on the &lt;code&gt;worker.create&lt;/code&gt; method. Here we focus on performance-related options.&lt;/p&gt;
&lt;h3 id=&quot;task-execution-and-polling&quot;&gt;Task execution and polling&lt;/h3&gt;
&lt;p&gt;Each worker has a number of pollers and slots for each &lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; task type &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;workflow/activity/local-activity/nexus&lt;/span&gt; &lt;/span&gt; . The pollers grab tasks from the Temporal server, and occupy a slot while the task is being executed.
The reasoning behind having multiple pollers and not just one is due to performance. From the code docs:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;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;In general, a Workflow Worker&amp;#39;s performance is mostly network bound (due to&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;communication latency with the Temporal server). Accepting multiple Workflow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Tasks concurrently helps compensate for network latency, until the point&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;where the Worker gets CPU bound.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are two ways to configure this: &lt;br/&gt;
The original way, using maxConcurrent&lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; &amp;lt;taskType&amp;gt; &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;workflow/activity/local-activity/nexus task&lt;/span&gt; &lt;/span&gt; Executions and maxConcurrent&lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; &amp;lt;taskType&amp;gt; &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;workflow/activity/local-activity/nexus task&lt;/span&gt; &lt;/span&gt; Polls. &lt;br/&gt;
The new way, using the &lt;code&gt;tuner&lt;/code&gt;, which grants slots until a targetCpu and targetMemory are reached.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;We will focus on workflow and activity tasks, which are the majority of tasks that are executed.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;threading-model&quot;&gt;Threading model&lt;/h3&gt;
&lt;p&gt;The threading model is explained &lt;a href=&quot;https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#threading-model&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;. &lt;br/&gt;
Basically, interactions with the Core SDK happen on the main thread (Node’s event loop). There’s also at least one worker thread, which the main thread delegates workflow tasks to. This is a great idea, as workflow tasks are CPU bound (they run until the next await).&lt;/p&gt;
&lt;p&gt;The number of worker threads is configured with the &lt;code&gt;workflowThreadPoolSize&lt;/code&gt; option. It defaults to 1 if &lt;code&gt;reuseV8Context&lt;/code&gt; is true, or 2 otherwise.&lt;/p&gt;
&lt;p&gt;Workflow tasks tend to be lightweight, so the defaults are usually fine.&lt;/p&gt;
&lt;h3 id=&quot;sticky-tasks-and-worker-cache&quot;&gt;Sticky tasks and worker cache&lt;/h3&gt;
&lt;p&gt;There’s also the &lt;code&gt;nonStickyToStickyPollRatio&lt;/code&gt;, which indicates the ratio of pollers that will poll from the sticky task queue. &lt;br/&gt;
Along with &lt;code&gt;maxCachedWorkflows&lt;/code&gt;, it is a very useful optimization to avoid having to replay workflows.&lt;/p&gt;
&lt;p&gt;The values should be set according to your workload patterns. &lt;br/&gt;
The docs on it are good, so no need to go deeper here.&lt;/p&gt;
&lt;h2 id=&quot;what-can-we-do-to-fix-the-problem&quot;&gt;What can we do to fix the problem?&lt;/h2&gt;
&lt;h3 id=&quot;task-execution-and-polling-1&quot;&gt;Task execution and polling&lt;/h3&gt;
&lt;p&gt;By far the most relevant configuration is related to task execution and polling.&lt;/p&gt;
&lt;p&gt;Looking at metrics for number of &lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; slots &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;slots are tasks being executed concurrently&lt;/span&gt; &lt;/span&gt; , we can see that the number of workflow slots is directly related to the number of tasks failing, and through the previous relationship, the number of workflow tasks timing out.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/failures-slots.png&quot; alt=&quot;workflow task failures and slots&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Workflow task failures and slots&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;This image is obtained using the &lt;code&gt;tuner&lt;/code&gt;, targeting 0.8 CPU and 0.6 memory usage, with a workflow task &lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; rampThrottle &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;time before handling new slots&lt;/span&gt; &lt;/span&gt;  of 100ms. Without the rampThrottle, we would get a lot of “out of memory” errors, as the worker would fill its memory trying to execute too many tasks at once.&lt;/p&gt;
&lt;p&gt;It’s obvious from the image that the tuner is granting too many slots, and that this for some reason, is causing tasks to fail.&lt;/p&gt;
&lt;p&gt;The worker takes more than the start-to-close timeout to complete the tasks, so when responding to the Temporal server with a result, the task is no longer found, and the result is discarded. This is very bad, as that same task is reissued, and the process repeats, burning CPU cycles for no reason.&lt;/p&gt;
&lt;div style=&quot;background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 1rem 1.5rem; margin: 2rem 0; border-radius: 4px;&quot;&gt;&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; Workflow tasks take so long to complete, not because they are expensive
(they take &lt;em&gt;tens of milliseconds&lt;/em&gt;), but because there are many slots executing concurrently along with the
main thread.&lt;/p&gt;&lt;/div&gt;
&lt;h3 id=&quot;threading-model-1&quot;&gt;Threading model&lt;/h3&gt;
&lt;p&gt;In all my tests, the tuner grants way too many workflow slots no matter what &lt;code&gt;targetCPU&lt;/code&gt; is specified. To understand why, we need to look at the threading model.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/50+pollers-cpu.png&quot; alt=&quot;50+ slots CPU usage&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;CPU usage for &lt;code&gt;targetCPU=0.5&lt;/code&gt; (50+ slots)&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;This is running on ECS Fargate, on single vCPU instances.&lt;/p&gt;
&lt;p&gt;Running 2 vCPU instances, with the default configuration, the tuner was &lt;em&gt;slightly more&lt;/em&gt; useful, but still granted too many slots.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/tuner-2vcpu.png&quot; alt=&quot;2 vCPUs slots and task failures&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;2 vCPUs slots and task failures&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;When configuring a fixed sized workflow task slot supplier, with 2 vCPUs and one worker thread, I could get a bit less than twice the number of slots as with 1 vCPU, but maximum CPU usage was less than 75%, which makes sense, the worker thread was at full capacity, and the main thread was half-idle, mostly due to I/O bound work.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/2vCPU-CPU.png&quot; alt=&quot;2 vCPUs CPU usage&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;2 vCPUs CPU usage&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;I couldn’t get the tuner to work as expected, so I can’t recommend using it, it causes too many workflow task slots, which makes them time out and be retried, burning CPU cycles for no reason.&lt;/p&gt;
&lt;p&gt;The most important thing is to use a fixed sized workflow task slot supplier, with a size that is a good fit for your workload.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/failures-slots.png&quot; alt=&quot;workflow task failures and slots&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Workflow task failures and slots&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;If no configuration is used (no tuner, no maxConcurrentWorkflowTaskExecutions), the worker will use the default configuration, which is 40 workflow slots (likely still way too many).&lt;/p&gt;
&lt;h2 id=&quot;why-is-the-default-worker-configuration-so-bad&quot;&gt;Why is the default worker configuration so bad?&lt;/h2&gt;
&lt;p&gt;Well, It’s not that bad… &lt;br/&gt;
For parallel programming languages (like go and java, which are likely Temporal’s target languages), it makes sense, as we want to reduce the percentage of I/O work, and there’s no event loop or main thread that limits performance.&lt;/p&gt;
&lt;p&gt;I have explored more in detail the tuner, running on 2vCPU instances, and poller configuration, which you can read about in the &lt;a href=&quot;/humanity/temporal/worker-configuration/deep-dive/&quot;&gt;deep-dive&lt;/a&gt; article.&lt;/p&gt;
&lt;h2 id=&quot;a-decent-worker-configuration&quot;&gt;A decent worker configuration&lt;/h2&gt;
&lt;p&gt;&lt;del&gt;We now use&lt;/del&gt; A good enough configuration is the following:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    ...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; other things&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    tuner &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        workflowTaskSlotSupplier: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            type: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;fixed-size&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            numSlots: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;X&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; for conversation, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; for enrich),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        activityTaskSlotSupplier: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            type: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;resource-based&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            rampThrottle: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;100 ms&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            tunerOptions: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                targetCpuUsage: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1.0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                targetMemoryUsage: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0.6&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // local and nexus tasks don&amp;#39;t really matter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    nonStickyToStickyPollRatio: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Y&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; for conversation, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0.5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; for enrich),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    maxCachedWorkflows: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Z&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;150&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; for conversation, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;300&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; for enrich),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difference between conversation and enrich is that conversation workflow tasks are slower, due to replay, and more of the conversation workflows end up not being sticky.&lt;/p&gt;
&lt;p&gt;Values for activity tasks are not very relevant, as our workflows don’t fan out into many activities, and eager dispatch is enabled. &lt;br/&gt;
Activities are mostly processed instantly, and are very cheap, though they do sit in the event loop, adding a bit of latency to the main thread.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/saturated-wftask-latency.png&quot; alt=&quot;workflow tasks latency&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/saturated-wftasks-failures.png&quot; alt=&quot;workflow tasks failures&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Workflow tasks latency and failures with 6 slots&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Running the same load, we can see that with 6 slots, there are no failures, and that the &lt;strong&gt;maximum&lt;/strong&gt; time that workflow tasks take to process is close to the start-to-close timeout.
With this configuration, 100% of users sending messages succeed.&lt;/p&gt;
&lt;h3 id=&quot;why-not-just-1-slot&quot;&gt;Why not just 1 slot?&lt;/h3&gt;
&lt;p&gt;Even though the default is not good, the docs are correct in that worker performance is mostly network bound so it’s still useful to have multiple slots. We want to make sure that either the workflow thread or the main thread are never idle due to having no work to do.&lt;/p&gt;
&lt;h2 id=&quot;running-workers-in-production&quot;&gt;Running workers in production&lt;/h2&gt;
&lt;p&gt;This article is related to worker configuration, but to run workers in production, infrastructure and scaling strategies also need to be considered.&lt;/p&gt;
&lt;p&gt;Workers should run on fixed-size instances and scale horizontally. (We use ECS Fargate, and run 1-50 (usually under 10) instances per service, larger teams probably use Kubernetes)&lt;/p&gt;
&lt;p&gt;Scaling should be done on average CPU usage across worker instances, and either task backlog count or backlog age. (We send task queue metrics to CloudWatch)&lt;/p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;
&lt;p&gt;If you are interested in understanding the configuration and the details of the many experiments I have run, you can read the &lt;a href=&quot;/humanity/temporal/worker-configuration/deep-dive/&quot;&gt;deep-dive&lt;/a&gt; article.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Boxing</title>
    <link>https://miquelpuigturon.com/self/sport/boxing/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/self/sport/boxing/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Boxing The first part was originally hand-written in Catalan Why Do I Box? It’s a question I often ask myself. I don’t do it for the reasons one would imagine: I don’t care about the ‘aura’ it gives…</description>
    <content:encoded>&lt;h1 id=&quot;boxing&quot;&gt;Boxing&lt;/h1&gt;
&lt;details&gt;&lt;summary style=&quot;cursor: pointer; margin-bottom: 1rem;&quot;&gt;The first part was originally hand-written in Catalan&lt;/summary&gt;&lt;p&gt;&lt;img src=&quot;/self/sport/boxing/page-1.jpg&quot; alt=&quot;Page 1&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;/self/sport/boxing/page-2.jpg&quot; alt=&quot;Page 2&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;/self/sport/boxing/page-3.jpg&quot; alt=&quot;Page 3&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;/self/sport/boxing/page-4.jpg&quot; alt=&quot;Page 4&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;/self/sport/boxing/page-5.jpg&quot; alt=&quot;Page 5&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;/self/sport/boxing/page-6.jpg&quot; alt=&quot;Page 6&quot;/&gt;&lt;/p&gt;&lt;/details&gt;
&lt;h2 id=&quot;why-do-i-box&quot;&gt;Why Do I Box?&lt;/h2&gt;
&lt;p&gt;It’s a question I often ask myself.&lt;/p&gt;
&lt;p&gt;I don’t do it for the reasons one would imagine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I don’t care about the ‘aura’ it gives me&lt;/li&gt;
&lt;li&gt;It doesn’t help me relax&lt;/li&gt;
&lt;li&gt;Even though physical superiority makes me feel good, it doesn’t compensate for the bitter aftertaste of being the one who inflicts pain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is what is most clear to me. Makes it hard to justify following through.&lt;/p&gt;
&lt;p&gt;Every day, I wake up and go back to it. &lt;br/&gt;
Boxing forces me to face myself: am I enough, will I still show up knowing I’ll get hit, and do I have the competitive drive to keep improving.&lt;/p&gt;
&lt;h2 id=&quot;prologue&quot;&gt;Prologue&lt;/h2&gt;
&lt;p&gt;The boxing club is in the building across from where I live. &lt;br/&gt;
I used to look at it with curiosity.&lt;/p&gt;
&lt;p&gt;Walking with a friend, the topic came up. I said I’d like to try it out. &lt;br/&gt;
My main reason was the nobility of the sport. Also, I thought knowing how to fight was worth something.&lt;/p&gt;
&lt;p&gt;My friend said she couldn’t imagine me doing it. That I was incapable of hurting someone, and I’d lack the malice both to perform and to have the will to do it.&lt;/p&gt;
&lt;h2 id=&quot;beginning&quot;&gt;Beginning&lt;/h2&gt;
&lt;p&gt;Boxing has more technique than I expected. The first half of the year I didn’t fight. I remember when I asked for it, the first sparrings were light. People had good faith, and they wanted to help.&lt;/p&gt;
&lt;p&gt;At this moment, I thought I was a decent fighter. I saw what I could improve on, and I was ready to work on it. &lt;br/&gt;
I was ecstatic about the decision of boxing, which so far confirmed the nobility of the sport.&lt;/p&gt;
&lt;h2 id=&quot;reality-check&quot;&gt;Reality Check&lt;/h2&gt;
&lt;p&gt;The coach left, and a guy who trained with us, became the new coach. &lt;br/&gt;
He is one of the people I like most from everyone I’ve met. We’ve barely spoken outside of boxing, I know nothing about him.&lt;/p&gt;
&lt;p&gt;The first painful sparring I remember was with a fat guy, who was as inexperienced as me, but didn’t have the blocker I have of not wanting to cause harm. I went a few days with a headache, and that was it.&lt;/p&gt;
&lt;p&gt;Then an interclub came up, and with my coach’s encouragement I signed up. I approached it with a mix of desire and confidence, with the mentality of thanking the opponent to fight with me, to do a slightly harder sparring. &lt;br/&gt;
He went out to kill. I got hit, and I struggled to defend myself to reduce the damage.&lt;/p&gt;
&lt;p&gt;That day, I wrote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The fight was disappointing. I wasn’t beaten up, but I didn’t fight either. I could look for a thousand excuses, but there’s no need. &lt;br/&gt;
Noted for the next one to do better.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;end&quot;&gt;End?&lt;/h2&gt;
&lt;p&gt;Since the interclub, I’ve been boxing every day.&lt;/p&gt;
&lt;p&gt;I had my nose broken, I’ve bled more times than I can count on one hand. &lt;br/&gt;
The worst moment was when I made the other bleed. Noticing it made me not throw any punches. I felt awful.&lt;/p&gt;
&lt;p&gt;Now another interclub is coming. My trainer recommended I sign up. I did it.&lt;/p&gt;
&lt;p&gt;To train for it, my coach invited me to a neighbour club to spar. It was tuesday. &lt;br/&gt;
That day I woke up, trained boxing, and the afternoon I went to the club. I got beaten up. They were better, and as a souvenir, they gave me pain dripping with blood. &lt;br/&gt;
I got home feeling like crying, what hurt most was the realization of my inferiority. I called my mom, couldn’t tell her as I didn’t want her to worry. &lt;br/&gt;
That night the pillow soothed my crying.&lt;/p&gt;
&lt;p&gt;I have less desire and I am less confident, but I’m also more prepared than the first time.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;div style=&quot;text-align: center; font-style: italic; margin: 4rem 0;&quot;&gt;&lt;hr/&gt;&lt;p&gt;I wrote everything until here the night before the interclub&lt;/p&gt;&lt;hr/&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;h2 id=&quot;post-interclub&quot;&gt;Post Interclub&lt;/h2&gt;
&lt;p&gt;I crushed him. I was superior. If it were Sant Jordi, I would have made a rose out of his bleeding.&lt;/p&gt;
&lt;p&gt;I am confused. On one hand, I am proud. On the other hand, I feel awful. &lt;br/&gt;
Between rounds I told my trainer that I didn’t want to hurt him. The fight was stopped a couple of times to wipe the blood off his nose. I didn’t hit him as much as I could.&lt;/p&gt;
&lt;p&gt;It doesn’t make sense to continue boxing. The nobility is permanently stained of violence much like his white shirt with blood.&lt;/p&gt;
&lt;p&gt;I don’t enjoy victories. I assume them. Maybe that’s why losses bother me so much.&lt;/p&gt;
&lt;p&gt;Since I stopped competing at tennis, there was something uneasy within me. I wasn’t competitive, I accepted defeat.&lt;/p&gt;
&lt;p&gt;Today I wanted to win.&lt;/p&gt;
&lt;p&gt;I have shut down the inquietude, but I have sparked a fire inside me.&lt;/p&gt;
&lt;p&gt;I want to compete.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;div style=&quot;text-align: center; font-style: italic; margin: 4rem 0;&quot;&gt;&lt;hr/&gt;&lt;p&gt;2 weeks later&lt;/p&gt;&lt;hr/&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;h2 id=&quot;decision&quot;&gt;Decision&lt;/h2&gt;
&lt;p&gt;Today I announced I was leaving the club. It was shocking to my trainer, but it felt right.&lt;/p&gt;
&lt;p&gt;The main driver behind the decision is that I want to compete. Next step would be an amateur fight, but I don’t want to do it. I am not scared, but in boxing, higher level means more damage. I don’t want to inflict damage.&lt;/p&gt;
&lt;p&gt;Also, I have ambitious goals in running and lifting, and going to box every day is not helping me reach them. Especially lifting heavy, as the tension of the fight fries my CNS.&lt;/p&gt;
&lt;p&gt;Finally, I have always said that when I move out of my current flat, I would stop boxing, and I’ll likely leave in the next months. &lt;br/&gt;
I’m just getting ahead and making it my choice instead of waiting for life to take care of it.&lt;/p&gt;
&lt;p&gt;Boxing has been a great reminder of who I am. &lt;br/&gt;
Ambitious, capable, and decisively naïve about how the world actually works.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>OOM investigation &gt; Enrich deep dive</title>
    <link>https://miquelpuigturon.com/humanity/temporal/oom-investigation/deep-dive/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/temporal/oom-investigation/deep-dive/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>OOM investigation &gt; Enrich deep dive A sequel to the conversation worker OOM : same class of problem, no single-line fix. After fixing the conversation worker, I expected the enrich worker to be…</description>
    <content:encoded>&lt;h1 id=&quot;oom-investigation--enrich-deep-dive&quot;&gt;OOM investigation &gt; Enrich deep dive&lt;/h1&gt;
&lt;p&gt;A sequel to the &lt;a href=&quot;/humanity/temporal/oom-investigation/&quot;&gt;conversation worker OOM&lt;/a&gt;: same class of problem, no single-line fix.&lt;/p&gt;
&lt;p&gt;After fixing the conversation worker, I expected the enrich worker to be boring. It wasn’t crashing as dramatically, but it had the same smell: memory climbed with cached workflows and the bundle was much larger than it should have been.&lt;/p&gt;
&lt;p&gt;The difference was that this time the problem didn’t collapse into &lt;code&gt;&quot;sideEffects&quot;: false&lt;/code&gt;. Instead, it turned into a bundle experiment log.&lt;/p&gt;
&lt;p&gt;I started a &lt;a href=&quot;https://temporalio.slack.com/archives/C01DKSMU94L/p1772534214550679&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;thread in the Temporal Slack&lt;/a&gt; while working through this.&lt;/p&gt;
&lt;h2 id=&quot;why-we-started-looking&quot;&gt;Why We Started Looking&lt;/h2&gt;
&lt;p&gt;The previous incident taught us something important: &lt;strong&gt;for Temporal TypeScript workers, the workflow bundle is not just a build artifact. It is part of the runtime memory model.&lt;/strong&gt; &lt;br&gt;
So after that fix, I looked at our other Temporal service, which acts as a background jobs runner for many types of workflows: enrich.&lt;/p&gt;
&lt;p&gt;The numbers were suspicious:&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;Service&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Bundle size&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Cached WFs&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;conversation&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~2.93 MiB&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~700&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;enrich&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~14.1 MiB&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~150&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;That immediately put the bundle under suspicion.&lt;/p&gt;
&lt;p&gt;The top contributors in the enrich bundle were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zod@4&lt;/code&gt; at ~1 MB&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ai@6&lt;/code&gt; at ~435 KB&lt;/li&gt;
&lt;li&gt;workflow tool definitions at ~350 KB&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@enginy/common&lt;/code&gt; at ~300 KB&lt;/li&gt;
&lt;li&gt;workflow utils at ~227 KB&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tldts&lt;/code&gt; at ~150 KB&lt;/li&gt;
&lt;li&gt;a huge hardcoded &lt;code&gt;isPersonalEmail&lt;/code&gt; domain set at ~126 KB&lt;/li&gt;
&lt;li&gt;Prisma browser runtime&lt;/li&gt;
&lt;li&gt;HTML parsing dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-first-hypothesis&quot;&gt;The First Hypothesis&lt;/h2&gt;
&lt;p&gt;The obvious theory: each cached workflow gets its own copy of too much code, so bundle size is directly drives RSS.&lt;/p&gt;
&lt;p&gt;That fit the observed failure shape. Workers would start, the sticky cache would fill, and memory would rise with it.&lt;/p&gt;
&lt;h2 id=&quot;bundle-inspection&quot;&gt;Bundle Inspection&lt;/h2&gt;
&lt;p&gt;The first pass was static analysis. I looked at the built workflow bundle and the import graph, then traced the biggest paths back to source.&lt;/p&gt;
&lt;h3 id=&quot;syncqueue-pulled-in-far-too-much&quot;&gt;syncQueue pulled in far too much&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;syncQueue.ts&lt;/code&gt; imported multiple CRM adapters directly. Those adapters brought in &lt;code&gt;tldts&lt;/code&gt;, HTML parsing packages, string similarity helpers, &lt;code&gt;isPersonalEmail&lt;/code&gt;, and the Prisma browser runtime.&lt;/p&gt;
&lt;p&gt;The workflow wasn’t just orchestrating activities anymore. It was pulling a large chunk of adapter logic into workflow code.&lt;/p&gt;
&lt;h3 id=&quot;ai-workflows-pulled-ai--zod-into-the-bundle&quot;&gt;AI workflows pulled ai + zod into the bundle&lt;/h3&gt;
&lt;p&gt;Some workflows intentionally use the AI SDK in-workflow. That is a real requirement, but it means the bundle includes &lt;code&gt;ai&lt;/code&gt;, &lt;code&gt;zod&lt;/code&gt;, lots of tool definitions, and large prompt/schema-related code. Legitimate, but expensive.&lt;/p&gt;
&lt;h2 id=&quot;can-we-fix-it-without-touching-workflow-code&quot;&gt;Can We Fix It Without Touching Workflow Code?&lt;/h2&gt;
&lt;p&gt;Before refactoring any workflow logic, I wanted to see how much we could get just by changing how the bundle is produced.&lt;/p&gt;
&lt;h3 id=&quot;aggressive-webpack-optimization&quot;&gt;Aggressive Webpack Optimization&lt;/h3&gt;
&lt;p&gt;The first temptation: just optimize harder. &lt;code&gt;minimize: true&lt;/code&gt;, custom Terser config, changing source map mode, more aggressive webpack optimization flags, externalizing modules.&lt;/p&gt;
&lt;p&gt;This is where Temporal becomes opinionated.&lt;/p&gt;
&lt;p&gt;Temporal workflow bundling is not a generic webpack build. It is part of the workflow sandbox. If you change too much, you can break workflow type names, stack traces, source maps, replay/debugging expectations, and possibly bundler assumptions used by the reusable VM executor.&lt;/p&gt;
&lt;p&gt;In particular, &lt;code&gt;minimize: true&lt;/code&gt; is unsafe unless function names are preserved, and replacing Temporal’s source map configuration is risky. &lt;br&gt;
This is due to how workflows are resolved by name.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Promising in theory, not safe enough as a default production fix. I did not want to solve an OOM by introducing a determinism or workflow registration problem.&lt;/p&gt;
&lt;h3 id=&quot;ignoremodules--externalization&quot;&gt;ignoreModules / Externalization&lt;/h3&gt;
&lt;p&gt;If some heavy packages should only exist at activity runtime, maybe we can tell the bundler to ignore them.&lt;/p&gt;
&lt;p&gt;This works only if the workflow bundle does not actually need those modules at evaluation time. That turned out to be the key limitation: if a workflow directly imports code that imports &lt;code&gt;tldts&lt;/code&gt;, you cannot simply ignore &lt;code&gt;tldts&lt;/code&gt;. If &lt;code&gt;syncQueue&lt;/code&gt; directly imports CRM adapter code, those adapter dependencies are part of the workflow module graph. If AI workflows use &lt;code&gt;ai&lt;/code&gt; and &lt;code&gt;zod&lt;/code&gt; inside workflow code, those packages cannot be externalized away.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Useful only for modules that should not have been in workflow code in the first place. Not a general solution.&lt;/p&gt;
&lt;h3 id=&quot;strip-sourcescontent-from-inline-source-maps&quot;&gt;Strip sourcesContent From Inline Source Maps&lt;/h3&gt;
&lt;p&gt;Temporal wants inline source maps, and changing source map behavior broadly is risky. But stripping &lt;code&gt;sourcesContent&lt;/code&gt; while keeping line/column mappings intact is a narrower change: you still get pointers to the original file/line, but the full embedded source text is removed from the bundle.&lt;/p&gt;
&lt;p&gt;This reduced ~150 MB of base memory and I haven’t noticed any difference in practice - sourcemap pointers still work, so errors show the same file and line information.&lt;/p&gt;
&lt;p&gt;A smaller win compared to what could be achieved, but it was the cleanest no-refactor change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; ~150 MB base memory reduction with no visible downsides.&lt;/p&gt;
&lt;h2 id=&quot;understanding-what-temporal-actually-reuses&quot;&gt;Understanding What Temporal Actually Reuses&lt;/h2&gt;
&lt;p&gt;This was the eureka moment.&lt;/p&gt;
&lt;p&gt;I patched the Temporal runtime locally to log what landed in the shared module cache vs the workflow-specific module cache.&lt;/p&gt;
&lt;p&gt;The behavior is roughly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The workflow bundle wrapper is loaded once in the reusable VM&lt;/li&gt;
&lt;li&gt;Temporal SDK internals and a small set of known-safe modules are reused&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User workflow modules are evaluated into module objects per workflow cache entry&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External libraries imported by user workflow code are generally treated as workflow-specific as well&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What gets reused is not “all imported libraries.” It’s mainly the shared VM context, the bundle scaffold, and Temporal runtime internals. &lt;br&gt;
Reusing those blindly could be problematic. If a module has mutable top-level state: a counter, a cache, a configuration object… sharing it across workflows would mean one workflow’s execution could silently affect another’s. The SDK can’t know which modules are safe to share, so it defaults to giving each cached workflow its own copy of everything.&lt;/p&gt;
&lt;p&gt;That explained everything. The bundle exists once, but &lt;strong&gt;most of its modules do not&lt;/strong&gt;. A small workflow that only needs to verify an email was getting its own in-memory copy of the AI SDK, &lt;code&gt;zod&lt;/code&gt;, all the CRM adapter code, the &lt;code&gt;isPersonalEmail&lt;/code&gt; domain set, and everything else in the bundle - just because the workflow barrel imported everything eagerly.&lt;/p&gt;
&lt;h2 id=&quot;lazy-workflow-loading&quot;&gt;Lazy Workflow Loading&lt;/h2&gt;
&lt;p&gt;Once that was clear, the goal became: stop every workflow from eagerly importing every other workflow’s dependency tree.&lt;/p&gt;
&lt;h3 id=&quot;first-attempt-rewriting-the-bundle&quot;&gt;First attempt: rewriting the bundle&lt;/h3&gt;
&lt;p&gt;The first idea was to rewrite the workflow entry file to only export the workflows each type actually needs. But mapping workflow type names back to their source files is error prone, and rewriting the bundle seems too hacky, so I decided against it.&lt;/p&gt;
&lt;h3 id=&quot;second-attempt-lazy-imports&quot;&gt;Second attempt: lazy imports&lt;/h3&gt;
&lt;p&gt;Next I tried dynamic &lt;code&gt;import()&lt;/code&gt; to lazy-load heavy workflows on demand. This didn’t work well either: dynamic imports caused webpack to split the bundle, which breaks Temporal’s expectation of a single self-contained bundle file.&lt;/p&gt;
&lt;h3 id=&quot;what-worked-require&quot;&gt;What worked: require()&lt;/h3&gt;
&lt;p&gt;The solution was using &lt;code&gt;require()&lt;/code&gt; instead of &lt;code&gt;import()&lt;/code&gt;. Since the bundle is already a single file evaluated in a V8 context, &lt;code&gt;require()&lt;/code&gt; resolves within the bundle without triggering code splitting.&lt;/p&gt;
&lt;p&gt;This meant the workflow entry file had to be a CommonJS file (&lt;code&gt;.cjs&lt;/code&gt;), but that was a small price. The entry file maps workflow type names to &lt;code&gt;require()&lt;/code&gt; calls, so each workflow only loads its own dependency tree when it’s actually needed.&lt;/p&gt;
&lt;p&gt;I decided to try this with the workflows that are most run, as reducing their memory footprint means reducing it for most of the cached workflows.&lt;/p&gt;
&lt;p&gt;The best candidates for lazy loading were workflows with large and distinct dependency trees: &lt;code&gt;syncQueue&lt;/code&gt;, AI fill workflows and workflow runners that pull many other workflows transitively.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; lazyWorkflows&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  syncQueue: [() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;./workflows/syncQueue&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;syncQueue&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  fillAiFieldsWorkflow: [() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;./workflows/fillAiFieldsWorkflowV2&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;fillAiFieldsWorkflow&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&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;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getWorkflowExport&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;workflowType&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (workflowType &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;default&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflowType &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;__esModule&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; undefined&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&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;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; lazyWorkflow&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; lazyWorkflows[workflowType]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (lazyWorkflow) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;loadModuleFn&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exportName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; lazyWorkflow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; workflowModule&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; loadModuleFn&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflowModule[exportName]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&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;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;./workflows/index&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)[workflowType]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&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;color:#79B8FF&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Proxy&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  {},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;_target&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;prop&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; typeof&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; prop &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;string&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getWorkflowExport&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(prop) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; undefined&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I inspected module-cache counts for specific workflows after the change, targeted workflows loaded far fewer modules than the fallback path.&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;Workflow&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Workflow-specific modules&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;syncQueue&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~27&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;verifyLeadEmailWorkflow&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~19&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;enrichLeadWorkflow&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~22&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;Fallback (eager path)&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;~280&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Lazy workflow registration doesn’t make workflow code shared, but it drastically reduces how much code each workflow pulls into its module cache. &lt;br&gt;
That was the real win.&lt;/p&gt;
&lt;p&gt;Next steps will be to lazy load all workflows, though I prefer keeping it simple so other developers don’t have to understand all of this, and can keep using the simple: &lt;code&gt;export * from myWorkflow&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-result&quot;&gt;The Result&lt;/h2&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;
  &lt;img src=&quot;/humanity/temporal/workflow-bundle/enrich/enrich-memory-utilization.png&quot; alt=&quot;enrich ECS memory utilization dropping from ~63% to ~35% after deployment&quot; style=&quot;max-width: 100%;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;
    Max memory utilization
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Memory utilization max dropped from ~100% with 150 cached workflows (can be seen at ~65% with ~100) to ~35% after the deployment, also with 150 cached workflows.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;
  &lt;img src=&quot;/humanity/temporal/workflow-bundle/enrich/enrich-sticky-cache-size.png&quot; alt=&quot;enrich sticky cache size recovering from ~90 to ~150 cached workflows&quot; style=&quot;max-width: 100%;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;
    Max cached workflows
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;We ended up with a layered fix:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Strip &lt;code&gt;sourcesContent&lt;/code&gt; from the enrich workflow bundle’s inline source maps (not a major win, but a safe starting point)&lt;/li&gt;
&lt;li&gt;Introduce lazy workflow loading via a &lt;code&gt;.cjs&lt;/code&gt; entry file using &lt;code&gt;require()&lt;/code&gt; for the most common workflows&lt;/li&gt;
&lt;li&gt;Trim barrel exports so the fallback path is less expensive&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These changes improved behavior without updating workflow code, so transparent to other developers.&lt;/p&gt;
&lt;h2 id=&quot;what-temporal-can-improve&quot;&gt;What Temporal can improve&lt;/h2&gt;
&lt;p&gt;Two things would make this class of problem much easier to solve:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;User module opt-in freezing.&lt;/strong&gt; If a module is known to be stateless (e.g. &lt;code&gt;zod&lt;/code&gt;, &lt;code&gt;ai&lt;/code&gt;, utility libraries), the SDK could allow users to mark it as safe to reuse across cached workflows instead of evaluating it into each workflow’s module cache. An opt-in freeze list would let the shared VM context actually share the expensive modules, which is what most users expect &lt;code&gt;reuseV8Context&lt;/code&gt; to do.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Better support for per-workflow bundle splitting.&lt;/strong&gt; The lazy &lt;code&gt;require()&lt;/code&gt; approach works, but it’s a workaround. First-class support for registering workflows with separate bundles (or bundle segments) would let teams isolate heavy dependency trees without the &lt;code&gt;.cjs&lt;/code&gt; entry file hack. The underlying mechanism already seems to work; making it an officially supported pattern would make this much easier to reason about.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>incidentd - Architecture</title>
    <link>https://miquelpuigturon.com/humanity/fire/architecture/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/fire/architecture/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>incidentd - Architecture incidentd was built around a simple idea: strong atomic guarantees + durable eventual consistency , independent of where inputs come from or where side-effects go. It must be…</description>
    <content:encoded>&lt;h1 id=&quot;incidentd---architecture&quot;&gt;incidentd - Architecture&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;incidentd&lt;/code&gt; was built around a simple idea: &lt;br&gt;
&lt;strong&gt;strong atomic guarantees + durable eventual consistency&lt;/strong&gt;, independent of where inputs come from or where side-effects go.&lt;/p&gt;
&lt;p&gt;It must be incident-free.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;incidentd&lt;/code&gt; is a standalone service that manages an incident end-to-end: from declaration to resolution. It owns state, notifications, AI suggestions, status page updates, and post-incident analysis.&lt;/p&gt;
&lt;p&gt;From day one, it was designed to feed context into AI agents. That means data lives close, and reads/writes are fast.&lt;/p&gt;
&lt;h2 id=&quot;platform&quot;&gt;Platform&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;incidentd&lt;/code&gt; runs on Cloudflare Workers, using 3 primitives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Durable Objects&lt;/strong&gt; -&gt; per-incident transactional state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D1&lt;/strong&gt; -&gt; queryable indexing and listing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflows&lt;/strong&gt; -&gt; async side effects and AI jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Working on Cloudflare has been surprisingly good: great DX, trivial deployment and low cost.&lt;/p&gt;
&lt;p&gt;The only real downside is ISPs blocking thanks to a spectacularly bad ruling that gave power to LaLiga to control the internet. (irrelevant in this case since this is server-to-server)&lt;/p&gt;
&lt;h2 id=&quot;the-durable-object&quot;&gt;The Durable Object&lt;/h2&gt;
&lt;p&gt;Each incident maps to a single Durable Object.&lt;/p&gt;
&lt;p&gt;This is the source of truth for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Event timeline (&lt;code&gt;event_log&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Derived state: status, severity, assignee, metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every mutation follows the same pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start transaction&lt;/li&gt;
&lt;li&gt;Update state&lt;/li&gt;
&lt;li&gt;Append event (&lt;code&gt;published_at = NULL&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Schedule alarm&lt;/li&gt;
&lt;li&gt;Commit&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is the &lt;strong&gt;outbox pattern&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The core idea behind &lt;code&gt;incidentd&lt;/code&gt; is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A single-threaded, transactional system that gives you atomicity and reliable async delivery, without distributed coordination.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;alarm-based-delivery&quot;&gt;Alarm-based delivery&lt;/h3&gt;
&lt;p&gt;The DO alarm drains unpublished events:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; 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;for each event where published_at IS NULL and attempts &amp;#x3C; 3:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dispatch to IncidentWorkflow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    set published_at = now&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If dispatch fails, it retries. After 3 attempts, the event is marked failed but acknowledged. &lt;br&gt;
This gives at-least-once delivery with bounded retries.&lt;/p&gt;
&lt;p&gt;The same alarm also drives agent scheduling. A single method &lt;code&gt;decideAlarmAction()&lt;/code&gt; decides what to do next:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dispatch events&lt;/li&gt;
&lt;li&gt;trigger an agent turn&lt;/li&gt;
&lt;li&gt;schedule cleanup&lt;/li&gt;
&lt;li&gt;or do nothing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps the reliability story local: one place decides what must happen next, and one durable log says what already happened.&lt;/p&gt;
&lt;h3 id=&quot;the-dispatcher&quot;&gt;The dispatcher&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;IncidentWorkflow&lt;/code&gt; bridges the incident Durable Object and the senders.
It’s a long-lived workflow that processes events in order:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; 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;dispatch first event&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;while incident is not resolved or declined:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    wait for next event&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dispatch it&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each event is fanned out to all senders using &lt;code&gt;Promise.allSettled&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A failing sender doesn’t block others&lt;/li&gt;
&lt;li&gt;Failures are logged, not fatal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because delivery is at-least-once, senders should be idempotent (&lt;code&gt;incident_id&lt;/code&gt; + &lt;code&gt;event_id&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Events are committed sequentially in the DO, and dispatched in the same order. &lt;br&gt;
No races, no inconsistent external state.&lt;/p&gt;
&lt;h3 id=&quot;why-not-queues&quot;&gt;Why not Queues?&lt;/h3&gt;
&lt;p&gt;Queues have a classic problem: &lt;strong&gt;dual-writes&lt;/strong&gt;.
Update state in one system, enqueue in another. If enqueue fails, the event is lost.&lt;/p&gt;
&lt;p&gt;It can be fixed (outbox tables, polling, distributed transactions), but complexity explodes.&lt;/p&gt;
&lt;p&gt;Here, the outbox lives inside the Durable Object, and dispatch goes directly to a workflow.&lt;/p&gt;
&lt;p&gt;Also:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Workflows are basically queues with better ergonomics&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So I use them.&lt;/p&gt;
&lt;h3 id=&quot;ids-and-identifiers&quot;&gt;IDs and identifiers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Internal IDs: newUniqueId() → stored in D1&lt;/li&gt;
&lt;li&gt;DO resolution: idFromString&lt;/li&gt;
&lt;li&gt;External IDs: Slack, dashboard, etc. → stored as routing metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Originally I used idFromName (e.g. Slack thread ID), but that made IDs non-reusable after resolution.&lt;/p&gt;
&lt;h2 id=&quot;the-plugin-model&quot;&gt;The plugin model&lt;/h2&gt;
&lt;p&gt;The system is structured as:
&lt;strong&gt;receivers → handler → durable object → dispatcher → senders&lt;/strong&gt;&lt;/p&gt;
&lt;!--?xml version=&quot;1.0&quot; standalone=&quot;no&quot;?--&gt;

&lt;p&gt;&lt;svg version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1407.3340999271354 289.53139701580017&quot; width=&quot;700&quot; height=&quot;170&quot;&gt;&lt;!-- svg-source:excalidraw --&gt;&lt;metadata&gt;&lt;/metadata&gt;&lt;defs&gt;&lt;style class=&quot;style-fonts&quot;&gt;
@font-face { font-family: Excalifont; src: url(data:font/woff2;base64,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); }&lt;/style&gt;&lt;/defs&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(162.86294570021062 153.13158947275463) rotate(0 83.85032214759786 48.6463662859768)&quot;&gt;&lt;path d=&quot;M24.32 0 C51.79 -0.4, 78.64 -0.31, 143.38 0 M24.32 0 C72.65 1.35, 119.78 0.8, 143.38 0 M143.38 0 C158.65 -0.54, 165.9 8.64, 167.7 24.32 M143.38 0 C158.02 -1.33, 169.34 8.28, 167.7 24.32 M167.7 24.32 C168.2 41.84, 169.33 59.44, 167.7 72.97 M167.7 24.32 C167.98 36.2, 169.01 49.44, 167.7 72.97 M167.7 72.97 C169.08 90.45, 159.29 98.37, 143.38 97.29 M167.7 72.97 C166.86 88.73, 160.44 98.6, 143.38 97.29 M143.38 97.29 C106.29 94.53, 68.7 97.8, 24.32 97.29 M143.38 97.29 C113.8 96.41, 81.48 95.55, 24.32 97.29 M24.32 97.29 C7.49 99.24, 1.62 89.58, 0 72.97 M24.32 97.29 C8.81 96, -0.23 90.32, 0 72.97 M0 72.97 C-0.77 57.45, -0.73 44.65, 0 24.32 M0 72.97 C-0.41 57.98, -0.24 42.85, 0 24.32 M0 24.32 C-0.66 8.04, 7.87 0.53, 24.32 0 M0 24.32 C-0.35 6.47, 8.87 1.99, 24.32 0&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(214.4572940318905 191.77795575873142) rotate(0 32.25597381591797 10)&quot;&gt;&lt;text x=&quot;32.25597381591797&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Receiver&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(381.40246570622986 151.60516624169531) rotate(0 83.85032214759786 48.6463662859768)&quot;&gt;&lt;path d=&quot;M24.32 0 C54.22 1.9, 78.69 0.49, 143.38 0 M24.32 0 C60.07 -1.16, 94.3 -1.21, 143.38 0 M143.38 0 C159.24 1.93, 165.97 7.77, 167.7 24.32 M143.38 0 C158.01 -1.15, 168.56 9.17, 167.7 24.32 M167.7 24.32 C168.81 41.1, 166.26 56.43, 167.7 72.97 M167.7 24.32 C168.06 35.8, 168.79 48.95, 167.7 72.97 M167.7 72.97 C167.66 89.37, 158.79 97.06, 143.38 97.29 M167.7 72.97 C167.39 89.06, 160.9 99.43, 143.38 97.29 M143.38 97.29 C100.55 97.69, 62.06 97.96, 24.32 97.29 M143.38 97.29 C104.94 96, 65.93 96.34, 24.32 97.29 M24.32 97.29 C8 96.27, 1.67 88.33, 0 72.97 M24.32 97.29 C7.13 95.65, 0.27 87.04, 0 72.97 M0 72.97 C0.17 54.91, 2.26 40.83, 0 24.32 M0 72.97 C-0.76 62.16, -1.1 49.49, 0 24.32 M0 24.32 C1.22 8.36, 8.19 0.01, 24.32 0 M0 24.32 C0.19 9.56, 6.28 -0.32, 24.32 0&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(437.62080695783163 190.2515325276721) rotate(0 27.631980895996094 10)&quot;&gt;&lt;text x=&quot;27.631980895996094&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Handler&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(894.1405891123219 149.86217151644155) rotate(0 83.85032214759786 48.6463662859768)&quot;&gt;&lt;path d=&quot;M24.32 0 C66.97 0.07, 110.36 -0.72, 143.38 0 M24.32 0 C62.95 -0.29, 101.2 0.36, 143.38 0 M143.38 0 C160.04 1.94, 169.34 9.29, 167.7 24.32 M143.38 0 C161.22 1.71, 166.47 6.83, 167.7 24.32 M167.7 24.32 C168.86 40.85, 169.66 53.99, 167.7 72.97 M167.7 24.32 C167.15 42.86, 167.08 60.57, 167.7 72.97 M167.7 72.97 C168.88 88.08, 160.34 95.83, 143.38 97.29 M167.7 72.97 C166.07 90.5, 158.59 96.77, 143.38 97.29 M143.38 97.29 C108.41 96.58, 76.5 97.59, 24.32 97.29 M143.38 97.29 C108.33 97.98, 71.92 97.37, 24.32 97.29 M24.32 97.29 C9.03 97.15, 0.53 89.45, 0 72.97 M24.32 97.29 C9.06 95.15, -1.32 89.03, 0 72.97 M0 72.97 C-1.17 65.02, -0.63 54.28, 0 24.32 M0 72.97 C-0.01 59.01, -0.4 44.12, 0 24.32 M0 24.32 C-1.42 9.84, 9.24 -0.85, 24.32 0 M0 24.32 C1.53 9.7, 8.15 1.3, 24.32 0&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(935.966947331697 188.50853780241835) rotate(0 42.023963928222656 10)&quot;&gt;&lt;text x=&quot;42.023963928222656&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Dispatcher&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(10 29.51091381420349) rotate(0 76.69753666978187 57.74249180275939)&quot;&gt;&lt;path d=&quot;M96.25 14.5 C104.48 20.68, 117.1 29.84, 134.15 43.5 M96.25 14.5 C103.88 20.86, 111.89 26.06, 134.15 43.5 M134.15 43.5 C152.38 58.8, 152.73 56.52, 134.15 72.5 M134.15 43.5 C151.84 58.16, 154.16 56.92, 134.15 72.5 M134.15 72.5 C120.83 81.84, 112.4 91.09, 96.25 100.98 M134.15 72.5 C120.18 82.54, 106.82 93.26, 96.25 100.98 M96.25 100.98 C78.24 115.18, 75.27 116.08, 57.75 100.98 M96.25 100.98 C77.9 113.66, 79.15 114.75, 57.75 100.98 M57.75 100.98 C46.17 95.18, 37.93 86.81, 19.25 72.5 M57.75 100.98 C47.84 93.53, 40.07 86.33, 19.25 72.5 M19.25 72.5 C1.78 59.01, 0.75 57.83, 19.25 43.5 M19.25 72.5 C-1.7 59.38, 0.85 59.01, 19.25 43.5 M19.25 43.5 C30.67 34.84, 39.44 27.39, 57.75 14.5 M19.25 43.5 C32.48 32.38, 45.58 22.85, 57.75 14.5 M57.75 14.5 C76.37 -0.2, 75.82 -0.84, 96.25 14.5 M57.75 14.5 C78.03 -0.53, 75.95 -1.14, 96.25 14.5&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(53.8327923827425 67.38215971558316) rotate(0 33.01597595214844 20)&quot;&gt;&lt;text x=&quot;33.01597595214844&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;External&lt;/text&gt;&lt;text x=&quot;33.01597595214844&quot; y=&quot;34.096000000000004&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;event&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(112.9852608068935 123.2705663620801) rotate(0 21.947587406497867 37.85783222513771)&quot;&gt;&lt;path d=&quot;M0.35 0 C1.22 9.82, -2.52 45.36, 4.53 57.95 C11.59 70.54, 36.02 72.51, 42.69 75.55 M-0.93 -1.04 C-0.09 8.5, -3.68 43.18, 3.97 56.15 C11.61 69.12, 38.09 73.19, 44.96 76.76&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(112.9852608068935 123.2705663620801) rotate(0 21.947587406497867 37.85783222513771)&quot;&gt;&lt;path d=&quot;M22.91 77.18 C28.3 78.23, 34.71 74.93, 44.96 76.76 M22.91 77.18 C29.34 77.54, 35.38 76.27, 44.96 76.76&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(112.9852608068935 123.2705663620801) rotate(0 21.947587406497867 37.85783222513771)&quot;&gt;&lt;path d=&quot;M27.79 62.91 C31.88 67.46, 37.1 67.64, 44.96 76.76 M27.79 62.91 C32.92 67.32, 37.59 70.08, 44.96 76.76&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask&gt;&lt;/mask&gt;&lt;g stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(336.56358999540635 201.60753668570237) rotate(0 19.266968350664925 -0.5567354459547005)&quot;&gt;&lt;path d=&quot;M0.02 0.31 C6.56 -0.02, 32.62 -1.15, 39.16 -1.42 M-0.63 -0.01 C5.87 -0.04, 32.27 -0.72, 38.89 -0.85&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(336.56358999540635 201.60753668570237) rotate(0 19.266968350664925 -0.5567354459547005)&quot;&gt;&lt;path d=&quot;M20.79 6.21 C26.5 4.75, 30.71 2.89, 38.89 -0.85 M20.79 6.21 C26.24 4.42, 30.52 2.88, 38.89 -0.85&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(336.56358999540635 201.60753668570237) rotate(0 19.266968350664925 -0.5567354459547005)&quot;&gt;&lt;path d=&quot;M20.49 -7.08 C26.35 -4.72, 30.65 -2.76, 38.89 -0.85 M20.49 -7.08 C25.99 -5.24, 30.35 -3.15, 38.89 -0.85&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask&gt;&lt;/mask&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(625.7830542839494 148.34006074677222) rotate(0 56.70562500990002 51.43694262723545)&quot;&gt;&lt;path d=&quot;M58.38 -0.87 C67.64 -1.44, 79.08 2.64, 87.01 7.22 C94.94 11.8, 101.46 19.12, 105.97 26.63 C110.48 34.15, 114.12 43.65, 114.08 52.31 C114.03 60.97, 110.74 71.23, 105.71 78.61 C100.68 85.99, 92.45 92.49, 83.9 96.58 C75.36 100.66, 63.94 103.27, 54.42 103.13 C44.91 103, 34.72 100.22, 26.82 95.76 C18.92 91.3, 11.5 83.98, 7.03 76.37 C2.56 68.75, -0.3 58.59, -0.01 50.06 C0.28 41.54, 3.99 32.41, 8.78 25.21 C13.58 18.02, 20.07 11.01, 28.77 6.91 C37.48 2.81, 55.55 1.51, 61.01 0.62 C66.48 -0.27, 61.9 0.64, 61.57 1.56 M46.21 0.2 C54.77 -2, 65.5 -0.83, 74.58 2.16 C83.67 5.15, 94.36 11.44, 100.73 18.17 C107.1 24.9, 111.27 34.22, 112.79 42.54 C114.31 50.86, 113.17 59.88, 109.83 68.1 C106.5 76.31, 100.03 86.06, 92.78 91.85 C85.53 97.64, 75.66 101.54, 66.34 102.84 C57.02 104.13, 45.84 102.42, 36.86 99.63 C27.87 96.83, 18.59 92.66, 12.45 86.08 C6.31 79.5, 1.66 68.71, 0 60.14 C-1.66 51.56, -0.87 42.57, 2.48 34.65 C5.83 26.73, 12.75 18.19, 20.09 12.62 C27.42 7.04, 42.17 3.14, 46.51 1.19 C50.84 -0.76, 45.89 0.23, 46.11 0.92&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(650.071770511287 179.90559243878613) rotate(0 32.319976806640625 20)&quot;&gt;&lt;text x=&quot;32.319976806640625&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Incident&lt;/text&gt;&lt;text x=&quot;32.319976806640625&quot; y=&quot;34.096000000000004&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;DO&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(555.1031100014256 200.2241748382694) rotate(0 32.578059518795925 -0.848532647173954)&quot;&gt;&lt;path d=&quot;M-0.78 0.36 C9.79 0.37, 52.92 0.19, 63.72 -0.09 M1.02 -0.5 C11.92 -0.85, 55.61 -2.06, 65.93 -2.06&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(555.1031100014256 200.2241748382694) rotate(0 32.578059518795925 -0.848532647173954)&quot;&gt;&lt;path d=&quot;M42.58 6.88 C50.84 2.51, 58.4 0.1, 65.93 -2.06 M42.58 6.88 C47.03 5.59, 52.23 1.91, 65.93 -2.06&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(555.1031100014256 200.2241748382694) rotate(0 32.578059518795925 -0.848532647173954)&quot;&gt;&lt;path d=&quot;M42.3 -10.22 C50.63 -8.99, 58.28 -5.8, 65.93 -2.06 M42.3 -10.22 C46.66 -7.69, 51.92 -7.55, 65.93 -2.06&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask&gt;&lt;/mask&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(702.5826852101183 139.53139701580017) rotate(0 73.2139568912263 70)&quot;&gt;&lt;path d=&quot;M92.5 17.75 C102.75 25.48, 109.43 37.39, 127.93 53.25 M92.5 17.75 C102.73 28.73, 115.38 41.51, 127.93 53.25 M127.93 53.25 C146.04 72.45, 144.45 69.93, 127.93 88.75 M127.93 53.25 C147.01 71.6, 147.1 69.1, 127.93 88.75 M127.93 88.75 C117.57 99.16, 104.39 108.43, 92.5 122.25 M127.93 88.75 C115.37 100.96, 101.48 112.66, 92.5 122.25 M92.5 122.25 C75.65 138.97, 73.42 139.33, 55.5 122.25 M92.5 122.25 C72.08 140.14, 73.62 141.79, 55.5 122.25 M55.5 122.25 C46.53 112.86, 36.92 102.48, 18.5 88.75 M55.5 122.25 C40.31 109.99, 26.78 96.07, 18.5 88.75 M18.5 88.75 C0.88 70.21, -1.9 69.65, 18.5 53.25 M18.5 88.75 C-1.01 70.87, 2.05 69.19, 18.5 53.25 M18.5 53.25 C31.46 41.78, 45.16 28.99, 55.5 17.75 M18.5 53.25 C31.37 40.75, 45.7 27.6, 55.5 17.75 M55.5 17.75 C72.13 0.95, 75.77 1.68, 92.5 17.75 M55.5 17.75 C75.96 -1.92, 75.26 1.13, 92.5 17.75&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(746.9376952719423 179.53139701580017) rotate(0 28.751968383789062 30)&quot;&gt;&lt;text x=&quot;28.751968383789062&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Alarm&lt;/text&gt;&lt;text x=&quot;28.751968383789062&quot; y=&quot;34.096000000000004&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;/&lt;/text&gt;&lt;text x=&quot;28.751968383789062&quot; y=&quot;54.096000000000004&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Outbox&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(843.9615739494067 212.21013250232818) rotate(0 22.14171172692886 -5.839970791317853)&quot;&gt;&lt;path d=&quot;M0.29 0.34 C7.66 -1.73, 37.14 -9.89, 44.5 -12.02 M-0.22 0.04 C7.08 -1.97, 36.82 -9.28, 44.24 -11.3&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(843.9615739494067 212.21013250232818) rotate(0 22.14171172692886 -5.839970791317853)&quot;&gt;&lt;path d=&quot;M25.35 1.65 C32.57 -2.37, 38.67 -6.99, 44.24 -11.3 M25.35 1.65 C29.88 -0.81, 33.51 -3.83, 44.24 -11.3&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(843.9615739494067 212.21013250232818) rotate(0 22.14171172692886 -5.839970791317853)&quot;&gt;&lt;path d=&quot;M21.45 -13.52 C30.09 -12.21, 37.57 -11.49, 44.24 -11.3 M21.45 -13.52 C26.91 -12.5, 31.44 -12.06, 44.24 -11.3&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask&gt;&lt;/mask&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(1111.7651515301513 147.25747750715936) rotate(0 83.85032214759792 48.6463662859768)&quot;&gt;&lt;path d=&quot;M24.32 0 C70.74 -1.13, 115.02 1.6, 143.38 0 M24.32 0 C57.82 0.49, 92.9 0.88, 143.38 0 M143.38 0 C159.22 1.53, 168.09 8.38, 167.7 24.32 M143.38 0 C160.73 1, 168.24 10.17, 167.7 24.32 M167.7 24.32 C167.79 39.55, 168.26 55.64, 167.7 72.97 M167.7 24.32 C167.55 41.95, 167.34 61.61, 167.7 72.97 M167.7 72.97 C168.48 90.99, 158.49 96.4, 143.38 97.29 M167.7 72.97 C166.27 89.61, 159.54 96.57, 143.38 97.29 M143.38 97.29 C113.98 99.34, 84.33 97.57, 24.32 97.29 M143.38 97.29 C99.03 96.49, 54.53 95.53, 24.32 97.29 M24.32 97.29 C7.7 96.36, 1.77 90.57, 0 72.97 M24.32 97.29 C9.76 95.62, -0.56 87.9, 0 72.97 M0 72.97 C-1.77 59.5, 1.05 42.11, 0 24.32 M0 72.97 C-0.87 54.43, -0.43 36.81, 0 24.32 M0 24.32 C-0.57 7.27, 10.1 0.22, 24.32 0 M0 24.32 C-0.11 7.55, 8.27 -0.91, 24.32 0&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(1169.7034863730619 185.90384379313616) rotate(0 25.9119873046875 10)&quot;&gt;&lt;text x=&quot;25.9119873046875&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Sender&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(1067.8412334075176 198.15455435468357) rotate(0 22.689716450512606 -1.3739983426514755)&quot;&gt;&lt;path d=&quot;M-0.19 0.48 C7.47 -0.07, 37.92 -2.62, 45.57 -3.23 M0.72 0.25 C8.37 -0.26, 37.83 -2.27, 45.29 -2.78&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(1067.8412334075176 198.15455435468357) rotate(0 22.689716450512606 -1.3739983426514755)&quot;&gt;&lt;path d=&quot;M24.57 6.41 C30.15 4.91, 33.11 3, 45.29 -2.78 M24.57 6.41 C29 4.02, 33.8 2.77, 45.29 -2.78&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(1067.8412334075176 198.15455435468357) rotate(0 22.689716450512606 -1.3739983426514755)&quot;&gt;&lt;path d=&quot;M23.51 -9.06 C29.34 -7.04, 32.54 -5.44, 45.29 -2.78 M23.51 -9.06 C28.22 -8.07, 33.25 -5.95, 45.29 -2.78&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask&gt;&lt;/mask&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(1243.9390265875718 10) rotate(0 76.69753666978181 57.74249180275939)&quot;&gt;&lt;path d=&quot;M96.25 14.5 C108.94 22.53, 120.28 31.94, 134.15 43.5 M96.25 14.5 C103.73 20.28, 112.29 26.81, 134.15 43.5 M134.15 43.5 C152.43 57.38, 154.92 56.12, 134.15 72.5 M134.15 43.5 C155.34 57.21, 152.9 57.36, 134.15 72.5 M134.15 72.5 C118.17 83.81, 107.56 94.48, 96.25 100.98 M134.15 72.5 C125.01 80.86, 114.27 87.95, 96.25 100.98 M96.25 100.98 C77.3 116.16, 77.74 116.4, 57.75 100.98 M96.25 100.98 C76.02 115.46, 77.72 114.85, 57.75 100.98 M57.75 100.98 C50.27 96.18, 41.99 88.98, 19.25 72.5 M57.75 100.98 C48.98 93.67, 39.94 86.51, 19.25 72.5 M19.25 72.5 C1.28 58.32, -1.48 57.59, 19.25 43.5 M19.25 72.5 C-2.24 59.64, 1.49 59.38, 19.25 43.5 M19.25 43.5 C27.93 36.78, 40.54 26.34, 57.75 14.5 M19.25 43.5 C30.87 34.47, 41.91 26.61, 57.75 14.5 M57.75 14.5 C75.96 -0.71, 75.87 1.24, 96.25 14.5 M57.75 14.5 C78.33 1.27, 75.65 0.93, 96.25 14.5&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(1287.7718189703141 47.871245901379666) rotate(0 33.01597595214844 20)&quot;&gt;&lt;text x=&quot;33.01597595214844&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;External&lt;/text&gt;&lt;text x=&quot;33.01597595214844&quot; y=&quot;34.096000000000004&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;event&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(1285.4657958253474 195.69551504207027) rotate(0 41.314604846504494 -45.11364855084116)&quot;&gt;&lt;path d=&quot;M0.08 0.77 C13.08 0.38, 65.16 13.59, 78.17 -2.21 C91.18 -18, 78.28 -78.44, 78.14 -93.98 M-1.34 0.13 C11.58 -0.63, 64.67 11.97, 77.77 -4.01 C90.87 -20, 77.38 -80.71, 77.26 -95.8&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(1285.4657958253474 195.69551504207027) rotate(0 41.314604846504494 -45.11364855084116)&quot;&gt;&lt;path d=&quot;M88.38 -73.41 C86.18 -78.62, 82.44 -87.68, 77.26 -95.8 M88.38 -73.41 C83.78 -81.61, 79.09 -89.84, 77.26 -95.8&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(1285.4657958253474 195.69551504207027) rotate(0 41.314604846504494 -45.11364855084116)&quot;&gt;&lt;path d=&quot;M71.39 -71.5 C74.47 -77.32, 76.01 -86.97, 77.26 -95.8 M71.39 -71.5 C73.31 -80.37, 75.12 -89.33, 77.26 -95.8&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask&gt;&lt;/mask&gt;&lt;/svg&gt;&lt;/p&gt;
&lt;p&gt;Each stage is decoupled&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Receivers&lt;/strong&gt; -&gt; validate + normalize input&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The handler&lt;/strong&gt; -&gt; pushes events to the DO&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DO&lt;/strong&gt; -&gt; commits state + events atomically and schedules dispatch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The dispatcher&lt;/strong&gt; -&gt; delivers events&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Senders&lt;/strong&gt; -&gt; produce side-effects&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Adapters isolate integrations:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; 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;src/adapters/&amp;#x3C;name&gt;/receiver/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;src/adapters/&amp;#x3C;name&gt;/sender/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding a new integration means adding an adapter, not touching core logic.&lt;/p&gt;
&lt;h3 id=&quot;current-adapters&quot;&gt;Current adapters&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Dashboard&lt;/strong&gt; (&lt;code&gt;src/adapters/dashboard/&lt;/code&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Receiver: REST API&lt;/li&gt;
&lt;li&gt;Sender: writes to D1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Slack&lt;/strong&gt; (&lt;code&gt;src/adapters/slack/&lt;/code&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Receiver: events, interactions&lt;/li&gt;
&lt;li&gt;Sender: messages, threads, channels&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Status page&lt;/strong&gt; (&lt;code&gt;src/dispatcher/status-page.ts&lt;/code&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sender-only: persists affections to external DB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each adapter handles idempotency in its own way.&lt;/p&gt;
&lt;h2 id=&quot;the-ai-agent&quot;&gt;The AI agent&lt;/h2&gt;
&lt;p&gt;Each incident runs an agent that watches the timeline and generates suggestions.&lt;/p&gt;
&lt;p&gt;It helps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;resolve the incident&lt;/li&gt;
&lt;li&gt;keep stakeholders informed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The agent runs asynchronously. &lt;br&gt;
It reads from the timeline and writes back into it.&lt;/p&gt;
&lt;p&gt;It is more inspired by Codex than by a classic tool-calling loop: each turn is a &lt;code&gt;IncidentAgentTurnWorkflow&lt;/code&gt; run.&lt;/p&gt;
&lt;!--?xml version=&quot;1.0&quot; standalone=&quot;no&quot;?--&gt;

&lt;p&gt;&lt;svg version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 351.4273428697751 298.96879970742384&quot; width=&quot;350&quot; height=&quot;300&quot;&gt;&lt;!-- svg-source:excalidraw --&gt;&lt;metadata&gt;&lt;/metadata&gt;&lt;defs&gt;&lt;style class=&quot;style-fonts&quot;&gt;
@font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAA/8AA4AAAAAGuAAAA+mAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbhRIcNAZgAIEEEQgKpmCcbQsyAAE2AiQDYAQgBYMYByAbxhRRVLHCyL44jMFY+lghhUqlTaXaWVxzlYXzXm7XCtJ2fVD5JR6+X+N33t39ZppQ18p0y6IVUmRotE8qO8Tf/HvInKpJcWB5c7Wa0KTW2AdNlMFdIoqEa////Xy01uqyvMEjG8//mVil8A/elE5LarVz3YIhYdMRsO08VEdF99PL3c+qXRA4DglMycE7OSCStE3x9nFv8ruupY6XFpF0oIBE4Erd/y7VXqqW2oUPLICteS8YuwlwLChhvVhEKEt0a3sE7dIF7QGXj4RqYgwJbfMos3+leUnQTcYMGxBedLuawHHVVd3ouFXR0SJQlOw3ptysdrUAPC9OuUDaS6RT8EVEJ/QCUDQAxaOowotzla8UWsUIj9zwqD5FuQ2WiciN5mR2MbV6ITr/oZSteGPDp/5GPH9NvdjzzGOMHqztPZfMxZIamVypMuUqVKlWpwETF6/WUCuVMOisR5TXAmR2UO0G+VxqVLlE7RzpFc1nZsq5/ai/AiTJ0oyGEq+N1kXyorwJJGpk/NEnbNAEgNk6YxScn9kpN5LOALlfqHyy76aDpixwAIj0ZF3/RgHMZhDUJQLgoyV+4w/3xw+hU4bmM6e5EXVP2tE/LlKun4ZeKjObavVadekp/sOSbrNsOSrVauGafZ/FkoMO2GO7rZZbZpEF5gehzlQQoG7n+fIsCEpfAFD2Wp83h3SMztekW4pNiZw5zEnTeVd6BceKLdmqBGowO1cn9NQJvFXqonRTUfMIHbDYgMUCbuSEgZfEBd+g2GBPnkD2BVK9tXHqtZaX6vb9v3xjBvfKxOpkqmzWhnOzsSYBK+CcI84g2zVvSzRp2vnexq5b9yPMkYiQSb2e3sewH0PGr9H0wKN0JGXFj7dvB1ZAgPRIlOVZjl4KzwxjBWOBcWvCxaIbQjrw26wPY0yQI0uVI2C9RdyEbRijv8FdK6P7oLwUlTe3hEdhseSmOjRPjYgewThVTy4PWAgFKPljkk0CzG6DgT3Ik/CNLFeOHWON5XMi9dy89FkvJeMDcpJMgonVyZgQ1gYbJqci4qm8CgmDDgxmUByOiOflqAgiInskQkBGUnIEgsUM833GKDVogcICMyjTmvEQm7WzJkeqagU84Ijb0K9SuI0Zg4pRWvLmBk3rKSttCX6qIk+KHxU0F7WhAdVYhpjy7p1J7/H3124VWkr3rBh0H2trvv8g8wQym9mIjztPO6s7tUe64hmVX6iHuJt3nwML8SIHAUiy7ACH27C/60FcGOx4E2GO+jiZ2fWcyUPgk78jVZaRjCIiG1n9u4V4JS3RkEHWhCzOAutGTCF+pLs9cn81X/3wuJWo+POcm+KUVrpr3Elvr3UfyF70OPpM5OfP9pO7pzq21i/R0BC+D/0YxhFt5sMEJXh3qBlv+7DzNwiFGylpLDRyAqeVKoMPtn1yi7yaK5b3I2IFQdlSU1y9TXKHIcZgSAt0YBg5Wm+jqtUvGdamVBj9QVbRAV3B6Whpaub1wedqe/Sws9pxMjakvsAtLFbg4Mc6s+kg53SQlKd4RzN3DWp0jcBumteVhstJloxOjU5f8GaIp/TZZf4QOeNVs2rHGU0gcxYT5EBm0NKz+otIvHhDzafAIopZaKSTmOfVUcVkOXqrBOfiY75Bz5RmaHPJyeWz5eUno6y2Z1shrsWUcgA4DywLRM/z3pnSqAftSubWJwrDbq5V7gEL8ZedqEWi/1RVS7/Rz3XiUTqyoPY+BtZhE5oGxeJE+KBfYZAxjrUoYN9bfXerOgjWIzwdZ2Jo3lnXL7BrtLSU5tKY/6Utw/4uZtANQm+4DUlm1LyaW0gnJ4WUknPgw+EJmMmY4ghYq58m44nHzNBj/mbhJxy28t7jIpeczon4ft6Ef/hrulikqc3Ne9t51px25pDZtDUXyrWm14bTkGjJ/Nw1KIyBxRm7dED3FGnkhN7oISvraGZTk7dvE9nrkd5OGSzybCJwwLTmQObX3r1+SyNTokEeTz0GVnI4KcTABv67KUspv3Bd0cWpaRT0IWvDBxlmT2t8EQV//7f3+cn3zY5mwrZBQ2wMKpU+ZH04m+UOsxlkBn2Wq7fqrbxXlgHiXDNrU0ePvO3y5eXgPRhdlh4OK/SOouvi1t6Liq3ASvTHmq7QN+zh+5DZ6GGyOhH76hu1tXJtZY2418qnYW/ExT6kAyyEi8UM2Xv/JIF62ztZnqBi9D6qbl8eakN/EEO6DVbDUeZ6HpQlgdh/B68oeTY7C/vtL6fY4BoNQyxc0RLzegxh2BXPsDvvKn26rk+blD0il0FCHxz2tdn1PODAWqt4dD6RwAKSB6gzZMKmmQxPV1iNCtx9tSH1uUda+ZDSvdV5nmYTzM50OogT+Vzelj2PeKP/rZVFDhqHMlRsSluYIGOnp7TGSkt5Xe6VZ6XKC7eLX7l0uVYCi3O0mifaWECmq7LsyYhMZckP0UyHwXYhBoZqvNjvDXSgWRB+mMn/RRPU8NtPIPsB/ezWVcv//3Pa8sdjiJr+/ti/2IHA2smPsfJdVVXL6St8WebLpXnfOhd+mRdu0P1kF0ODoFbnBvZugLtO8/XGQhDsEM6kwsyJh4f7QyRo5TOpIpORisHbWLNOLsV+LXWKtO3Xt1RJI0De7E+x7o60wDkX6EyFEoYGXdyhtc/j1BdN0hZqPGvbRVi4i47Cj8WGHy39A/ueyIb3C+t0DDqA/m4x1NlGl1xkEIvfvP82LZyRZVZqWjwnA+QAdgrCohrtbkGPZUA6H6QOfMTvHFZ1ZRO8wdxm3bwZi2F0/iqsJRphIg9/yEQ4AoGhXTxz4KbYux6kxT90ohEB1BEWu9Fq9FXvLLtpenDpZvnRRxqfKsYYml+38W3v+cSaJDnTaMdhLLK0vMphRS2eOH3UHkiTBBWiWlZqS1oAIWcPKOiI7jOxVmALX8HYaaC1pTyl4vXEDipjS6YSrwyt6J/BAfQjzo3zwhbaZBcmPOCn3rNerfXf9AHLQoRyhK6jI5AeGiR67cSrsSh8wtxEYUCTDqlW4YJGVdxysTFSRIrhOHymgRDxqWRyGT0zcFiozSvF0TMoBCrhDphcwPXE6vQRaigCP9VXkC5JqSnJDLf6pOVTAwznKh728GTnPPQXQ/OyJriFUmdAI7RsZe6GzDDO3OTMgMglte4BBz6LK00l4ZCWWJjmMS7jGZwTtNMdE6MSUrba59ixc7DNLIff+FBjKWdzOsjb8uPxSX5Vq9ZvrIKfEc8DohpscvpEGPwewWxnPFZ/PdzFGwpItuxPWM/wmxS5GNU8VPk+AOROB0z0AIJoh7Mky1cV+dIjHgZMl0GrvgSooTTCqwnTcwwye3Ng7wTa9bkb2sxSqDey48HoASbgevtLe3CqY5iahJkwDC6GzKw66bo+yq2ZnkTswdnpPu0TVMlZQ0u/ahKaq8FGgU5RDWVhWOjJCGqx1KynZ0P92rz/D1oOLcHrlyN4eg6vGKfGVA9FFjjpGCcxBXqYhoZijWIfPDST7lzoJC8lszDqx2rMK1weuIAnEmYtQvj9JbvpR0FxzQGh5VMl/fYvnbFl/CBuWeAL9XFxzcVk/+MHkEtfm5emvpz4MUi5G9bGL/ks/50JbWrEuJQ4fk8JWMaUnby17ifWkogjZA/ulUV+mBaUhOAQNw4r5VZlMemlcMKOD1aP6UpgHiE3+vlk+qkrIvM8PdRp3q9VOHbzO0FLC82Uevi4colzq5j3PS8scSpnIWwi2KoVIPDIEb+ahiRLBv9T5YE2S3IAailAuBxF072T1OWOjMGsgcdv61z+EvajYWauJRvVBut9C191Y9q/aohecM4Txbg2Dvu0vHYhYD9hGaMLm2erBVkVPOZu/aTvEgT8G0TILGWiFnhclCx+eFwKUZVf9z0r2gQwVVISpnubwfz3U6NRoo+PT11Vs4tx20+kZZf54LvmIsuH+QGPZXOyfeqp3aKq2XfCXoCPLfiAK6EuHk33YndwozK1jkK+RuKNkAAkp0QqpHA6EymCudQ/EyxMjHZi+QYIFOdG2B/9vBZXYSx0WhVcrjiF7ZM/3zM/BIpmGtrA+IOPfnpFxGWDKlgMs3WqeoS8l68bimXkMYbzzywBlB4PnBmXjxN6vQw4ep7T3Ho5HC+JhODI4AawGSYKmDW1TE1xwLoUngSLF2uiVVZWRFbbQv0en1TSVC9MJkKg6z6IDjeEUqMh3aarvrmtvrfU4sUNExo3eZvsTAtyEBK+EtWBO2hI/b7U32h9Q0j9Ok3QjOS0EcnHFca1yc19Pb9WO6Wi9KBcbmqMkaRZUAtJbBFhPkd3MWwT9fjAUBJEbP6wqRGM951M1bMxGyVknkJCgRYv4C0dUzSojI0hCMemsMkrTH1HEln+o4CrlTImNn7pw4uJVCQV4cJ6oNnMuuofI3/ttWSjmuR5arKqK3xeptYu1KiMjkse2ObnxX45GFiMfFzYrO9K4ViHYiwftnT+vn5l6Pf3m+0f86O1pY9wjDZxv36Avexikf+lbf6k3gHslFzdoRsEr42O2w90sax88oGY2h113CURgYizxl92a6Pm4tLVYGDFdyN9I6/ESIshZq8X/qtWSj+bN2uGGLrr+5jQTva6AxfvWtTlvaMzzW2ifuAWGYuL04x80b+ab4WTert9REs2DVMpVkzkQwaSnYKN+MaSyAaaEFEE7OcfF2k97wg6TPDak1bjmRYUc3YtDkHLF1PFkaEBMWgixfTpD7MA3MMxDu88GVMiMK/v5nkNBJIkBn77voLFTCdoHD58x7zPj/+E+HkL5gqvFV15loOdsVGEHsig4R7EaHr0NkPJmOi7mmWMKrFOqsHiwLbVC7NnNaL16YOya7QxUwo+Vuc+mBzO7c7Fo22T6SNWJ+RUqJEFHVZtrG7qtL6cq3SGVWRgRkEIHKJfWlOUqDZwuCkxDLfIcUXxK0z484Bl0JTxBdpXdCLMPTpKZ+ffjvxTft7Ez1B6Vhkt4SuehLwAgCV3iB4A/i963fv/kO+NvkJjAVAuXMT/pWtLmCaG+7PwP+1nas8cwJAmABDfAIjHgJeRQU2IgXf8rzF2AU20AEJWAKD0Q3+UgOAQAI8IBaERDVS+qA8qWB75QJrmAFn4gKAIBlD4AVlQQE1kA5qEdtcBFimQRCvr4KQyJyBSTgDccunhSprtc4Vav1yZScNcuWEOV4k1/FCqPzSwWY9KFZrUq9GqRYdwVtVqdWpSwSVXNZd29WaDRWQiSIXEM5m3uTnVYYQlBeQSQXOSZ40WzPdCRPN966uXwyQLF3c1X1/cFDopTm4T1NviupAQZAPBUuSkRyhvcJiuF7mVKi6UbhGcaDSJtSL33P6CaknV6NIHVYmAoMqKf0oAAAAA); }&lt;/style&gt;&lt;/defs&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(36.902450646776174 10) rotate(0 56.70562500990002 51.43694262723545)&quot;&gt;&lt;path d=&quot;M58.38 -0.87 C67.64 -1.44, 79.08 2.64, 87.01 7.22 C94.94 11.8, 101.46 19.12, 105.97 26.63 C110.48 34.15, 114.12 43.65, 114.08 52.31 C114.03 60.97, 110.74 71.23, 105.71 78.61 C100.68 85.99, 92.45 92.49, 83.9 96.58 C75.36 100.66, 63.94 103.27, 54.42 103.13 C44.91 103, 34.72 100.22, 26.82 95.76 C18.92 91.3, 11.5 83.98, 7.03 76.37 C2.56 68.75, -0.3 58.59, -0.01 50.06 C0.28 41.54, 3.99 32.41, 8.78 25.21 C13.58 18.02, 20.07 11.01, 28.77 6.91 C37.48 2.81, 55.55 1.51, 61.01 0.62 C66.48 -0.27, 61.9 0.64, 61.57 1.56 M46.21 0.2 C54.77 -2, 65.5 -0.83, 74.58 2.16 C83.67 5.15, 94.36 11.44, 100.73 18.17 C107.1 24.9, 111.27 34.22, 112.79 42.54 C114.31 50.86, 113.17 59.88, 109.83 68.1 C106.5 76.31, 100.03 86.06, 92.78 91.85 C85.53 97.64, 75.66 101.54, 66.34 102.84 C57.02 104.13, 45.84 102.42, 36.86 99.63 C27.87 96.83, 18.59 92.66, 12.45 86.08 C6.31 79.5, 1.66 68.71, 0 60.14 C-1.66 51.56, -0.87 42.57, 2.48 34.65 C5.83 26.73, 12.75 18.19, 20.09 12.62 C27.42 7.04, 42.17 3.14, 46.51 1.19 C50.84 -0.76, 45.89 0.23, 46.11 0.92&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(61.191166874113776 41.565531692013906) rotate(0 32.319976806640625 20)&quot;&gt;&lt;text x=&quot;32.319976806640625&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Incident&lt;/text&gt;&lt;text x=&quot;32.319976806640625&quot; y=&quot;34.096000000000004&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;DO&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(37.35060109881613 153.8432062621739) rotate(0 55.9812318572458 50)&quot;&gt;&lt;path d=&quot;M70 12.75 C73.88 19.77, 80.57 22.48, 97.96 38.25 M70 12.75 C79.08 21.24, 88.48 29.61, 97.96 38.25 M97.96 38.25 C113.11 50.06, 112.45 52.95, 97.96 63.75 M97.96 38.25 C113.22 49.81, 109.91 49.27, 97.96 63.75 M97.96 63.75 C92.36 70.17, 84.25 74.67, 70 87.25 M97.96 63.75 C88.58 71.4, 79.69 77.5, 70 87.25 M70 87.25 C56.21 98.78, 55.98 99.21, 42 87.25 M70 87.25 C55.51 101.22, 56.43 102, 42 87.25 M42 87.25 C34.26 81.57, 23.43 74.18, 14 63.75 M42 87.25 C32.89 80.87, 23.56 72.28, 14 63.75 M14 63.75 C0.49 50.04, 0.37 49.18, 14 38.25 M14 63.75 C-1.99 48.81, 1.99 48.92, 14 38.25 M14 38.25 C25.53 29.23, 34.1 19.97, 42 12.75 M14 38.25 C24.3 27.87, 36.2 16.9, 42 12.75 M42 12.75 C56.06 0.37, 57.02 1.96, 70 12.75 M42 12.75 C56.57 0.13, 54.01 -0.99, 70 12.75&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(70.56523130966559 183.8432062621739) rotate(0 22.775985717773438 20)&quot;&gt;&lt;text x=&quot;22.775985717773438&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Agent&lt;/text&gt;&lt;text x=&quot;22.775985717773438&quot; y=&quot;34.096000000000004&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Turn&lt;/text&gt;&lt;/g&gt;&lt;g mask=&quot;url(#mask-lXzHJnE9jnLDWONfnbsxh)&quot; stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(85.77759684667245 118.42428343238714) rotate(0 -23.046871052275463 30.725795668351537)&quot;&gt;&lt;path d=&quot;M-0.2 -0.15 C-7.95 4.84, -42 20.46, -45.54 30.7 C-49.07 40.93, -25.48 56.38, -21.41 61.25 M-1.76 -1.28 C-9.21 3.76, -40.3 21.01, -43.18 31.68 C-46.06 42.34, -22.69 57.93, -19.04 62.73&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(85.77759684667245 118.42428343238714) rotate(0 -23.046871052275463 30.725795668351537)&quot;&gt;&lt;path d=&quot;M-36.59 55.37 C-30.08 57.14, -24.83 59.45, -19.04 62.73 M-36.59 55.37 C-31.38 57.67, -25.83 59.74, -19.04 62.73&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(85.77759684667245 118.42428343238714) rotate(0 -23.046871052275463 30.725795668351537)&quot;&gt;&lt;path d=&quot;M-27.76 45.81 C-24.56 51.22, -22.68 57.17, -19.04 62.73 M-27.76 45.81 C-25.49 51.15, -22.81 56.34, -19.04 62.73&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask id=&quot;mask-lXzHJnE9jnLDWONfnbsxh&quot;&gt;&lt;rect x=&quot;0&quot; y=&quot;0&quot; fill=&quot;#fff&quot; width=&quot;230.64321401805194&quot; height=&quot;279.35723599333176&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;10&quot; y=&quot;140.17992713028434&quot; fill=&quot;#000&quot; width=&quot;61.82395935058594&quot; height=&quot;20&quot; opacity=&quot;1&quot;&gt;&lt;/rect&gt;&lt;/mask&gt;&lt;g transform=&quot;translate(10 140.17992713028434) rotate(0 52.73072579439699 8.970151970454339)&quot;&gt;&lt;text x=&quot;30.91197967529297&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;context&lt;/text&gt;&lt;/g&gt;&lt;g mask=&quot;url(#mask-V713An0IWjJcGrTz0NQWs)&quot; stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(143.77603096812834 204.7236098249084) rotate(0 22.621192628828624 -53.2260117935773)&quot;&gt;&lt;path d=&quot;M-0.23 0.14 C7.61 -8.75, 47.89 -34.95, 47.63 -52.74 C47.36 -70.53, 6.52 -97.59, -1.83 -106.59 M-1.82 -0.83 C5.92 -10.08, 47.4 -36.84, 47.31 -54.28 C47.21 -71.71, 5.88 -96.84, -2.39 -105.44&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(143.77603096812834 204.7236098249084) rotate(0 22.621192628828624 -53.2260117935773)&quot;&gt;&lt;path d=&quot;M21.28 -97.38 C13.25 -98.24, 6.66 -102.38, -2.39 -105.44 M21.28 -97.38 C14.28 -100.48, 8.78 -100.91, -2.39 -105.44&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(143.77603096812834 204.7236098249084) rotate(0 22.621192628828624 -53.2260117935773)&quot;&gt;&lt;path d=&quot;M10.56 -84.06 C6.2 -89.53, 3.33 -98.3, -2.39 -105.44 M10.56 -84.06 C6.53 -90.76, 3.96 -94.84, -2.39 -105.44&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask id=&quot;mask-V713An0IWjJcGrTz0NQWs&quot;&gt;&lt;rect x=&quot;0&quot; y=&quot;0&quot; fill=&quot;#fff&quot; width=&quot;292.4267971830136&quot; height=&quot;411.00357511958464&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;143.60637203564295&quot; y=&quot;140.86100828870542&quot; fill=&quot;#000&quot; width=&quot;93.21591186523438&quot; height=&quot;20&quot; opacity=&quot;1&quot;&gt;&lt;/rect&gt;&lt;/mask&gt;&lt;g transform=&quot;translate(143.60637203564295 140.86100828870542) rotate(0 22.790851561314014 10.636589742625688)&quot;&gt;&lt;text x=&quot;46.60795593261719&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Suggestions&lt;/text&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(225.5733581852213 196.05152301386727) rotate(0 35.522801640550824 31.600086113614054)&quot;&gt;&lt;path d=&quot;M33.74 -0.82 C40.21 -1.5, 49.64 1.51, 55.48 4.94 C61.33 8.38, 66.44 14.04, 68.79 19.77 C71.15 25.5, 71.42 33.02, 69.61 39.3 C67.79 45.57, 63.5 53.57, 57.91 57.42 C52.32 61.27, 43.01 62.39, 36.08 62.41 C29.14 62.43, 22.01 61.05, 16.3 57.56 C10.58 54.07, 4.35 47.14, 1.78 41.46 C-0.78 35.78, -1.04 29.25, 0.91 23.47 C2.85 17.69, 6.95 10.68, 13.46 6.8 C19.96 2.91, 34.6 0.94, 39.93 0.17 C45.26 -0.61, 45.36 1.49, 45.45 2.13 M41.44 0.38 C48.25 0.67, 55.1 3.71, 59.82 7.71 C64.54 11.7, 68.57 18.13, 69.74 24.33 C70.91 30.53, 69.64 39.06, 66.86 44.9 C64.08 50.75, 58.88 56.25, 53.08 59.4 C47.29 62.55, 39.18 64.78, 32.09 63.79 C25 62.79, 15.91 57.82, 10.54 53.43 C5.17 49.04, 0.83 43.55, -0.11 37.44 C-1.06 31.33, 1.87 22.25, 4.87 16.78 C7.87 11.31, 11.89 7.45, 17.89 4.62 C23.89 1.8, 37.29 0.6, 40.88 -0.17 C44.46 -0.93, 39.69 -0.43, 39.41 0.02&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(248.61313798326012 196.36346858260993) rotate(0 35.522801640550824 31.600086113614054)&quot;&gt;&lt;path d=&quot;M29.02 -0.03 C35.25 -1.79, 43.68 -0.7, 50 2.12 C56.32 4.94, 63.43 11.2, 66.95 16.87 C70.47 22.54, 72.17 30.12, 71.14 36.14 C70.11 42.17, 65.65 48.58, 60.77 53.01 C55.88 57.44, 48.62 61.48, 41.83 62.74 C35.03 64, 26.28 63.14, 19.99 60.57 C13.7 57.99, 7.38 52.7, 4.09 47.29 C0.8 41.88, -0.71 34.51, 0.23 28.12 C1.17 21.72, 3.61 13.58, 9.74 8.9 C15.87 4.23, 31.34 1.36, 37.01 0.08 C42.68 -1.19, 43.88 0.79, 43.74 1.24 M51.39 4.64 C57.38 7.21, 62.93 12.38, 66.23 17.53 C69.53 22.68, 71.87 29.56, 71.18 35.54 C70.49 41.52, 66.94 48.84, 62.07 53.41 C57.21 57.97, 48.98 61.76, 42.01 62.94 C35.04 64.12, 26.39 63.37, 20.24 60.51 C14.09 57.65, 8.28 51.5, 5.12 45.76 C1.96 40.02, 0.61 32.21, 1.3 26.09 C2 19.97, 4.3 13.16, 9.29 9.06 C14.28 4.97, 24.41 2.28, 31.24 1.51 C38.06 0.74, 47.01 4.12, 50.25 4.43 C53.49 4.74, 50.91 3.24, 50.69 3.36&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g stroke-linecap=&quot;round&quot; transform=&quot;translate(270.38173958867344 197.30450438164962) rotate(0 35.522801640550824 31.600086113614054)&quot;&gt;&lt;path d=&quot;M39.07 0.32 C45.79 0.51, 53.97 4.18, 59.15 8.28 C64.32 12.39, 68.75 19.08, 70.13 24.95 C71.5 30.81, 70.3 37.75, 67.39 43.47 C64.48 49.19, 58.4 55.99, 52.65 59.26 C46.9 62.54, 39.6 63.89, 32.89 63.11 C26.17 62.33, 17.72 58.61, 12.37 54.59 C7.02 50.56, 2.43 44.83, 0.79 38.93 C-0.84 33.04, -0.37 25.02, 2.57 19.21 C5.5 13.41, 11.64 7.17, 18.38 4.1 C25.13 1.02, 38.65 0.99, 43.03 0.77 C47.41 0.55, 44.94 2.17, 44.65 2.8 M48.48 1.74 C54.71 3.43, 61.77 9.48, 65.68 14.79 C69.6 20.09, 72.42 27.63, 71.98 33.57 C71.54 39.5, 67.37 45.72, 63.03 50.42 C58.7 55.11, 52.54 59.81, 45.96 61.74 C39.39 63.68, 30.31 63.95, 23.6 62.03 C16.9 60.1, 9.62 55.56, 5.74 50.18 C1.86 44.79, 0.13 36.11, 0.34 29.73 C0.55 23.35, 2.8 16.66, 6.98 11.89 C11.17 7.11, 18.81 2.54, 25.45 1.08 C32.08 -0.38, 43.01 2.75, 46.78 3.14 C50.56 3.52, 47.85 3.1, 48.09 3.38&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g mask=&quot;url(#mask-oogalCza_kFiL6OrzEZMX)&quot; stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(124.82533766702238 224.96367814348355) rotate(0 47.68285403574569 21.899667030066894)&quot;&gt;&lt;path d=&quot;M1.18 0.31 C6.61 7.67, 17.28 42.23, 32.92 43.21 C48.56 44.2, 84.76 12.38, 95.02 6.2 M0.34 -0.58 C5.62 7, 16.51 42.99, 32.17 44.34 C47.83 45.68, 83.92 13.69, 94.31 7.5&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(124.82533766702238 224.96367814348355) rotate(0 47.68285403574569 21.899667030066894)&quot;&gt;&lt;path d=&quot;M80.33 28.24 C85.58 21.92, 88.33 14.67, 94.31 7.5 M80.33 28.24 C84.61 21.5, 88.21 16.25, 94.31 7.5&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(124.82533766702238 224.96367814348355) rotate(0 47.68285403574569 21.899667030066894)&quot;&gt;&lt;path d=&quot;M70.28 14.41 C78.71 12.52, 84.75 9.8, 94.31 7.5 M70.28 14.41 C77.5 11.8, 84.1 10.68, 94.31 7.5&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask id=&quot;mask-oogalCza_kFiL6OrzEZMX&quot;&gt;&lt;rect x=&quot;0&quot; y=&quot;0&quot; fill=&quot;#fff&quot; width=&quot;319.7567340235777&quot; height=&quot;368.96879970742384&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;94.79290390787935&quot; y=&quot;248.96879970742384&quot; fill=&quot;#000&quot; width=&quot;125.16790771484375&quot; height=&quot;40&quot; opacity=&quot;1&quot;&gt;&lt;/rect&gt;&lt;/mask&gt;&lt;g transform=&quot;translate(94.79290390787935 248.96879970742384) rotate(0 77.71528779488872 -2.1054545338733988)&quot;&gt;&lt;text x=&quot;62.583953857421875&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Context &lt;/text&gt;&lt;text x=&quot;62.583953857421875&quot; y=&quot;34.096000000000004&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Provider Agents&lt;/text&gt;&lt;/g&gt;&lt;g mask=&quot;url(#mask--9UNWT-Fi8vzKmb8sr0fJ)&quot; stroke-linecap=&quot;round&quot;&gt;&lt;g transform=&quot;translate(285.33337941136574 197.12172420724505) rotate(0 -64.59168726166234 -66.88132948333731)&quot;&gt;&lt;path d=&quot;M0.82 0.72 C-1.53 -14.24, 7.86 -68.08, -13.96 -90.62 C-35.77 -113.15, -110.78 -127.39, -130.07 -134.48 M-0.21 0.05 C-2.64 -15.19, 6.9 -70.11, -14.35 -92.4 C-35.6 -114.69, -108.47 -127.11, -127.71 -133.69&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(285.33337941136574 197.12172420724505) rotate(0 -64.59168726166234 -66.88132948333731)&quot;&gt;&lt;path d=&quot;M-102.81 -135.99 C-109.42 -133.35, -117.3 -136.06, -127.71 -133.69 M-102.81 -135.99 C-107.77 -134.81, -113.81 -134.66, -127.71 -133.69&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;g transform=&quot;translate(285.33337941136574 197.12172420724505) rotate(0 -64.59168726166234 -66.88132948333731)&quot;&gt;&lt;path d=&quot;M-107.16 -119.45 C-112.29 -122.01, -118.82 -129.89, -127.71 -133.69 M-107.16 -119.45 C-111.09 -121.85, -116.19 -125.27, -127.71 -133.69&quot; stroke=&quot;#1e1e1e&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;mask id=&quot;mask--9UNWT-Fi8vzKmb8sr0fJ&quot;&gt;&lt;rect x=&quot;0&quot; y=&quot;0&quot; fill=&quot;#fff&quot; width=&quot;514.3996217049221&quot; height=&quot;430.59343421895005&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;237.719185552132&quot; y=&quot;95.8524618119767&quot; fill=&quot;#000&quot; width=&quot;65.21592712402344&quot; height=&quot;20&quot; opacity=&quot;1&quot;&gt;&lt;/rect&gt;&lt;/mask&gt;&lt;g transform=&quot;translate(237.719185552132 95.8524618119767) rotate(0 -16.977493402428593 34.387932911931046)&quot;&gt;&lt;text x=&quot;32.60796356201172&quot; y=&quot;14.096&quot; font-family=&quot;Excalifont, Xiaolai, sans-serif, Segoe UI Emoji&quot; font-size=&quot;16px&quot; fill=&quot;#1e1e1e&quot; text-anchor=&quot;middle&quot; style=&quot;white-space: pre;&quot; direction=&quot;ltr&quot; dominant-baseline=&quot;alphabetic&quot;&gt;Insights&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/p&gt;
&lt;h3 id=&quot;debouncing&quot;&gt;Debouncing&lt;/h3&gt;
&lt;p&gt;Not every event should trigger a turn.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First turn: delayed 60s (let the incident stabilize)&lt;/li&gt;
&lt;li&gt;Subsequent turns: 13s debounce window&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;execution&quot;&gt;Execution&lt;/h3&gt;
&lt;p&gt;Each turn:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fetch snapshot (state + events)&lt;/li&gt;
&lt;li&gt;Call LLM with tools&lt;/li&gt;
&lt;li&gt;Optionally trigger context agents&lt;/li&gt;
&lt;li&gt;Persist suggestions as events&lt;/li&gt;
&lt;li&gt;Post to Slack&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Suggestions are stored as internal events, so future turns have memory and avoid repetition.&lt;/p&gt;
&lt;h2 id=&quot;context-agents&quot;&gt;Context agents&lt;/h2&gt;
&lt;p&gt;Context agents are separate Durable Objects that run background investigations.&lt;/p&gt;
&lt;p&gt;That keeps the incident DO focused on coordination, not research.&lt;/p&gt;
&lt;p&gt;They are decoupled to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run asynchronously&lt;/li&gt;
&lt;li&gt;avoid polluting the main incident context&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;base-agent&quot;&gt;Base agent&lt;/h3&gt;
&lt;p&gt;Context agents extend &lt;code&gt;AgentBase&lt;/code&gt;, which provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQLite storage&lt;/strong&gt; -&gt; (&lt;code&gt;steps&lt;/code&gt;, &lt;code&gt;contexts&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Summarization&lt;/strong&gt; -&gt; keeps token usage bounded&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alarm-driven execution&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lazy initialization&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;bidirectional-rpc&quot;&gt;Bidirectional RPC&lt;/h3&gt;
&lt;p&gt;The incident DO and context agents communicate in both directions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-&gt; provider&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;addContext&lt;/code&gt;: pushes new timeline events for background processing&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addPrompt&lt;/code&gt;: handles user questions from Slack, pulling the provider’s latest findings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Provider -&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getAgentContext&lt;/code&gt;: pulls the full incident snapshot to inform investigation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;recordAgentInsightEvent&lt;/code&gt;: writes insight events (&lt;code&gt;SIMILAR_INCIDENT&lt;/code&gt;, &lt;code&gt;GITHUB_COMMIT&lt;/code&gt;) without &lt;code&gt;published_at&lt;/code&gt;, so they flow through the normal alarm/dispatch cycle and trigger new agent turns&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;current-providers&quot;&gt;Current providers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SimilarIncidentsAgent&lt;/code&gt;&lt;/strong&gt; -&gt; searches past incidents for similar patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;GitHubCommitsAgent&lt;/code&gt;&lt;/strong&gt; -&gt; surfaces relevant commits&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Future ones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS investigator (via CLI, on Container-enabled Durable Objects)&lt;/li&gt;
&lt;li&gt;DataDog (via MCP)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;prompt-workflow&quot;&gt;Prompt workflow&lt;/h2&gt;
&lt;p&gt;Users can directly ask the agent questions. Handled by &lt;code&gt;IncidentPromptWorkflow&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;LLM decides what to do&lt;/li&gt;
&lt;li&gt;If needed, calls context agents&lt;/li&gt;
&lt;li&gt;Executes action or responds&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A :fire: reaction shows progress.&lt;/p&gt;
&lt;p&gt;Expected usage is command-like (“resolve incident”, “post status page update”), making it easy for untrained responders.&lt;/p&gt;
&lt;h2 id=&quot;reliability&quot;&gt;Reliability&lt;/h2&gt;
&lt;p&gt;Everything optimizes for one goal:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;never lose an event&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Acknowledgement happens after persistence&lt;/li&gt;
&lt;li&gt;State + event are atomic&lt;/li&gt;
&lt;li&gt;Delivery is retried&lt;/li&gt;
&lt;li&gt;Senders are isolated&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So far: ** 0 lost events**, with low double-digit millisecond latency.&lt;/p&gt;
&lt;h2 id=&quot;folder-structure&quot;&gt;Folder structure&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; 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;- Adapters: src/adapters/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- Handler: src/handler&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- Core DO: src/core/incident.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- Alarm: src/core/incident/alarm.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- Dispatcher: src/dispatcher/workflow.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- Agents: src/agent/providers/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;The only part I’m still unsure about is how context agents will interact as their number grows.&lt;/p&gt;
&lt;p&gt;We’ll find out.&lt;/p&gt;</content:encoded>
  </item>
  <item>
    <title>Worker configuration (Node) &gt; Deep dive</title>
    <link>https://miquelpuigturon.com/humanity/temporal/worker-configuration/deep-dive/</link>
    <guid isPermaLink="true">https://miquelpuigturon.com/humanity/temporal/worker-configuration/deep-dive/</guid>
    <pubDate>Thu, 08 Oct 2026 13:07:43 GMT</pubDate>
    <description>Worker configuration ( Node ) &gt; Deep dive This article is a deep dive into the worker configuration and the many experiments I have run to understand it. It’s intended for those who want to…</description>
    <content:encoded>&lt;h1 id=&quot;worker-configuration-node--deep-dive&quot;&gt;Worker configuration (&lt;em&gt;Node&lt;/em&gt;) &amp;gt; Deep dive&lt;/h1&gt;
&lt;p&gt;This article is a deep dive into the worker configuration and the many experiments I have run to understand it. &lt;br/&gt;
It’s intended for those who want to understand the internals of Worker configuration. If you’re looking for a simpler setup or just want to know &lt;em&gt;why&lt;/em&gt; I ran these tests, start with the &lt;a href=&quot;/humanity/temporal/worker-configuration/&quot;&gt;main article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is not a definitive guide. Some parts remain unclear, but it documents what I’ve learned so far and where the documentation or defaults can be misleading.&lt;/p&gt;
&lt;div style=&quot;display: flex; gap: 2rem; margin: 1rem 0;&quot;&gt;&lt;div style=&quot;flex: 1;&quot;&gt;&lt;strong&gt;It focuses on:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;The tuner&lt;/li&gt;&lt;li&gt;Poller configuration&lt;/li&gt;&lt;li&gt;Worker thread configuration&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div style=&quot;flex: 1;&quot;&gt;&lt;strong&gt;It does NOT focus on:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;optimizing workflow/activity code&lt;/li&gt;&lt;li&gt;keeping history short&lt;/li&gt;&lt;li&gt;caching workflows / sticky vs non-sticky pollers&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-tuner&quot;&gt;The tuner&lt;/h2&gt;
&lt;p&gt;The &lt;em&gt;resource-based&lt;/em&gt; tuner aims to make Workers require almost no configuration. You specify a target CPU and memory usage, and the tuner will grant slots of each &lt;span class=&quot;tooltip-wrapper&quot; onmouseenter=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;block&apos;&quot; onmouseleave=&quot;this.querySelector(&apos;.tooltip&apos;).style.display=&apos;none&apos;&quot; data-astro-cid-ifo5z5xt&gt; task type &lt;span class=&quot;tooltip&quot; data-astro-cid-ifo5z5xt&gt;workflow/activity/local-activity/nexus&lt;/span&gt; &lt;/span&gt;  until the target CPU and memory usage are reached.&lt;/p&gt;
&lt;p&gt;In practice, I haven’t been able to make it behave as expected. Here’s what I found.&lt;/p&gt;
&lt;h3 id=&quot;under-reports-cpu-and-memory-usage&quot;&gt;Under-reports CPU and memory usage&lt;/h3&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/1vCPU-tuner-cpu-memory.png&quot; alt=&quot;Tuner CPU and memory usage&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/1vCPU-cloudwatch-cpu-memory.png&quot; alt=&quot;CloudWatch CPU and memory usage&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Tuner vs CloudWatch CPU and memory usage for 1 vCPU instance&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/2vCPU-tuner-cpu-memory.png&quot; alt=&quot;Tuner CPU and memory usage&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/2vCPU-cloudwatch-cpu-memory.png&quot; alt=&quot;CloudWatch CPU and memory usage&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Tuner vs CloudWatch CPU and memory usage for 2 vCPU instances&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;For both 1 and 2 vCPU instances, the tuner under-reports CPU and memory usage. &lt;br/&gt;
&lt;strong&gt;Hypothesis&lt;/strong&gt;: it fails to read cgroup stats, and is reading the Node process stats instead. &lt;br/&gt;
It then also under-reports those, by giving a reading of around half of Node’s.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/tuner-cpu-matches-node-cpu.png&quot; alt=&quot;Tuner CPU vs Node CPU&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Tuner CPU vs Node CPU&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Alternate hypothesis&lt;/strong&gt;: the tuner might be ignoring main-thread CPU usage.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/tuner-vs-nodecpu-elu.png&quot; alt=&quot;Tuner CPU vs Node CPU - ELU&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Tuner CPU vs Node CPU - ELU&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;em&gt;ELU&lt;/em&gt; (&lt;a href=&quot;https://nodesource.com/blog/event-loop-utilization-nodejs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;event loop utilization&lt;/a&gt;) measures how much time the main Node thread spends in the event loop. It’s a strong proxy for main-thread CPU usage.&lt;/p&gt;
&lt;h3 id=&quot;slot-grants-ignore-task-type-cost&quot;&gt;Slot grants ignore task type cost&lt;/h3&gt;
&lt;p&gt;The issue ties to the &lt;a href=&quot;/humanity/temporal/worker-configuration/#threading-model&quot;&gt;Worker threading model&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It’s NOT relevant in single-vCPU instances (where the main and worker threads share the same core) but becomes noticeable in multi-core setups.&lt;/p&gt;
&lt;p&gt;It depends on how expensive workflow vs activity tasks are, (Considering &lt;code&gt;rampThrottle&lt;/code&gt; and &lt;code&gt;minSlots&lt;/code&gt; configuration) you can see three possible behaviors:&lt;/p&gt;
&lt;h4 id=&quot;a-activity-tasks-are-more-expensive-than-workflow-tasks&quot;&gt;a) Activity tasks are ‘more expensive’ than workflow tasks&lt;/h4&gt;
&lt;p&gt;In this scenario, a backlog of activity tasks would accumulate.
The main thread is constantly scheduled on a core, yet the CPU usage would not reach high values as the worker thread is idle. &lt;br/&gt;
This causes the tuner to keep granting more activity task slots, which worsens the situation.&lt;/p&gt;
&lt;div style=&quot;margin: 2rem 0; padding: 2rem; background: #fafafa; border-radius: 8px; position: relative;&quot;&gt;&lt;div style=&quot;display: grid; grid-template-columns: 1fr auto 1fr; gap: 1rem; align-items: center;&quot;&gt;&lt;div style=&quot;border: 2px solid #333; padding: 1rem; border-radius: 4px; text-align: center; background: white;&quot;&gt;&lt;p&gt;Burst of tasks&lt;/p&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 1.5rem;&quot;&gt;→&lt;/div&gt;&lt;div style=&quot;border: 2px solid #333; padding: 1rem; border-radius: 4px; text-align: center; background: white;&quot;&gt;&lt;p&gt;Backlog accumulates&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;text-align: right; font-size: 1.5rem; padding: 0.5rem 0;&quot;&gt;↓&lt;/div&gt;&lt;div style=&quot;display: grid; grid-template-columns: 1fr auto 1fr; gap: 1rem; align-items: center;&quot;&gt;&lt;div style=&quot;border: 2px solid #d62828; padding: 1rem; border-radius: 4px; text-align: center; background: white;&quot;&gt;&lt;p&gt;Tuner grants more slots&lt;/p&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 1.5rem;&quot;&gt;←&lt;/div&gt;&lt;div style=&quot;border: 2px solid #333; padding: 1rem; border-radius: 4px; text-align: center; background: white;&quot;&gt;&lt;p&gt;CPU below target&lt;/p&gt;&lt;br/&gt;&lt;small&gt;(worker thread idle)&lt;/small&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;display: grid; grid-template-columns: auto 1fr; gap: 1rem; align-items: center;&quot;&gt;&lt;div style=&quot;text-align: left; font-size: 1.5rem; padding: 0.5rem 0;&quot;&gt;↓&lt;/div&gt;&lt;div style=&quot;text-align: right; font-size: 1.5rem; padding: 0.5rem 0; color: #d62828;&quot;&gt;↑&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;border: 2px dashed #d62828; padding: 1rem; border-radius: 4px; text-align: center; background: #ffe5e5;&quot;&gt;&lt;strong&gt;Main thread saturated&lt;/strong&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;b-workflow-tasks-are-more-expensive-than-activity-tasks&quot;&gt;b) Workflow tasks are ‘more expensive’ than activity tasks&lt;/h4&gt;
&lt;p&gt;This is a similar scenario to the previous one, but in reverse, a backlog of workflow tasks would accumulate.&lt;/p&gt;
&lt;p&gt;It can be solved by increasing the &lt;code&gt;workflowThreadPoolSize&lt;/code&gt; to match the number of cores, as if all worker threads are busy, CPU will reach the target, even if the main thread is idle.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/fixed-18-24-1-to-2-thread-worker-stats.png&quot; alt=&quot;Worker stats&quot; style=&quot;max-width: 100%; margin-bottom: 1rem;&quot;/&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/fixed-18-24-1-to-2-thread-tuner-stats.png&quot; alt=&quot;Tuner stats&quot; style=&quot;max-width: 100%; margin-bottom: 1rem;&quot;/&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/fixed-18-24-1-to-2-thread-node-stats.png&quot; alt=&quot;Node stats&quot; style=&quot;max-width: 100%; margin-bottom: 1rem;&quot;/&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/fixed-18-24-1-to-2-thrad-cloudwatch-stats.png&quot; alt=&quot;CloudWatch stats&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Stats comparison when increasing worker threads from 1 to 2&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;These images show the stats when running a workload saturated due to workflow tasks on fixed-sized tuner, going from 1 worker thread and 12 slots to 2 worker threads and 18 slots for workflow tasks and 24 slots for activity tasks.&lt;/p&gt;
&lt;p&gt;In the graphs, CPU plateaus near 80% with one worker thread (≈ main thread at 100% + worker thread at 50%) / 2. &lt;br/&gt;
Adding another thread (and more workflow slots) pushes CPU to 100%, confirming full utilization.&lt;/p&gt;
&lt;h4 id=&quot;c-both-are-equally-expensive&quot;&gt;c) Both are equally ‘expensive’&lt;/h4&gt;
&lt;p&gt;Even though this scenario is unrealistic, in this case the tuner performs well, slot distribution is balanced and increases until the target is reached.&lt;/p&gt;
&lt;h3 id=&quot;not-granting-slots-pollers&quot;&gt;Not granting slots? Pollers?&lt;/h3&gt;
&lt;p&gt;Given my workload is workflow-heavy, I tried setting a fixed size for workflow slots (to make sure not too many are picked and time out), and let the tuner manage activity tasks.&lt;/p&gt;
&lt;p&gt;For some reason, activity task slots used were low.&lt;/p&gt;
&lt;p&gt;An explanation could be not enough pollers. So I also tested:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Increasing pollers from 10 → 25 → 40&lt;/li&gt;
&lt;li&gt;Switching from fixed-size to &lt;code&gt;PollerBehaviorAutoscaling&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/activity-tuner-2cpu-8wftasks.png&quot; alt=&quot;Activity tuner with 8 workflow tasks&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Activity tuner with 8 workflow tasks&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/activity-tuner-2cpu-12wftasks.png&quot; alt=&quot;Activity tuner with 12 workflow tasks&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Activity tuner with 12 workflow tasks&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/activity-tuner-2cpu-6wftasks-40pollers.png&quot; alt=&quot;Activity tuner with 6 workflow tasks and 40 pollers&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Activity tuner with 6 workflow tasks and 40 pollers&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;None of these changed the number of activity task slots in use.&lt;/p&gt;
&lt;p&gt;When using a fixed-size number of activity task slots, more task slots were in use, meaning the tuner likely failed to grant slots.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/fixed-2cpu-8wf-24act.png&quot; alt=&quot;Fixed-size configuration with 2 CPU, 8 workflow tasks, and 24 activity tasks&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Fixed-size configuration: 8 workflow tasks, 24 activity tasks&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If the tuner measured resources correctly, it would work fine on 1 vCPU instances.&lt;/p&gt;
&lt;p&gt;On 2 vCPU instances, it could be useful for workflow-heavy workloads, assuming a fixed-size workflow task slot supplier and as many Worker threads as cores.&lt;/p&gt;
&lt;p&gt;I haven’t succeeded in using it reliably, but I’ve learned about the tuner, pollers, the threading model and how Node interacts with all of these.&lt;/p&gt;
&lt;p&gt;My current configuration (might go back to 1vCPU with a fixed-size workflow task slot supplier, which is simpler):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2 vCPUs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2 worker threads&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;14 slots for workflow tasks&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;24 slots for activity tasks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since we don’t fan out into many activities, activity slot count doesn’t matter much as long as there are enough slots. (24 or 24,000 would work just as well)&lt;/p&gt;
&lt;h2 id=&quot;appendix&quot;&gt;Appendix&lt;/h2&gt;
&lt;p&gt;If you can shed light on any of the unexplained behavior, please add a comment below or reach out at &lt;a href=&quot;mailto:miquel@miquelpuigturon.com&quot;&gt;miquel@miquelpuigturon.com&lt;/a&gt; or on Temporal Slack (&lt;code&gt;Miquel)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I have more graphs and data available for anyone interested.&lt;/p&gt;
&lt;figure style=&quot;margin: 2rem 0;&quot;&gt;&lt;img src=&quot;/humanity/temporal/worker-configuration/deep-dive/folders.png&quot; alt=&quot;Folder structure with additional data and images&quot; style=&quot;max-width: 100%;&quot;/&gt;&lt;figcaption style=&quot;text-align: center; font-style: italic; margin-top: 0.5rem; color: #666;&quot;&gt;&lt;p&gt;Additional data and images from experiments&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;</content:encoded>
  </item>
  </channel>
</rss>
