<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: אחיה כהן</title>
    <description>The latest articles on DEV Community by אחיה כהן (@achiya-automation).</description>
    <link>https://dev.to/achiya-automation</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810102%2Fefb43e59-992c-4f8b-91df-ee602c7c853f.jpg</url>
      <title>DEV Community: אחיה כהן</title>
      <link>https://dev.to/achiya-automation</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/achiya-automation"/>
    <language>en</language>
    <item>
      <title>I Logged Every AI Crawler for 34 Days. ChatGPT Outreads Googlebot</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:40:07 +0000</pubDate>
      <link>https://dev.to/achiya-automation/i-logged-every-ai-crawler-for-34-days-chatgpt-outreads-googlebot-369o</link>
      <guid>https://dev.to/achiya-automation/i-logged-every-ai-crawler-for-34-days-chatgpt-outreads-googlebot-369o</guid>
      <description>&lt;p&gt;In mid-July, my Google clicks in my home market (Israel) dropped by almost half. Buyer-intent queries that used to bring steady leads just evaporated from Search Console. While I was staring at GSC dashboards trying to figure out what broke, I finally did the thing I should have done months earlier: I stopped looking at dashboards and started reading raw server logs.&lt;/p&gt;

&lt;p&gt;What I found there was a parallel universe. Google Search was sending me less than ever — but AI systems were reading my site &lt;em&gt;constantly&lt;/em&gt;. Not "someday this will matter" constantly. Right-now constantly: an AI assistant was fetching a page of mine roughly every 26 minutes, around the clock, because a real human had just asked it a question.&lt;/p&gt;

&lt;p&gt;So I built a small log analyzer and let it run. Here's what 34 days of complete Caddy logs from a small business site (about 70 real human visitors a day) actually look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;All counts are HTTP 200 responses only (more on why below), over 34 days:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bot&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Per day&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bingbot&lt;/td&gt;
&lt;td&gt;5,444&lt;/td&gt;
&lt;td&gt;158.2&lt;/td&gt;
&lt;td&gt;Bing's index — which feeds ChatGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT-User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,388&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40.3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Live fetch while a human asks ChatGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Googlebot&lt;/td&gt;
&lt;td&gt;1,233&lt;/td&gt;
&lt;td&gt;35.8&lt;/td&gt;
&lt;td&gt;Classic Google crawl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPTBot&lt;/td&gt;
&lt;td&gt;547&lt;/td&gt;
&lt;td&gt;15.9&lt;/td&gt;
&lt;td&gt;OpenAI training crawler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude-User&lt;/td&gt;
&lt;td&gt;519&lt;/td&gt;
&lt;td&gt;15.1&lt;/td&gt;
&lt;td&gt;Live fetch while a human asks Claude&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAI-SearchBot&lt;/td&gt;
&lt;td&gt;281&lt;/td&gt;
&lt;td&gt;8.2&lt;/td&gt;
&lt;td&gt;ChatGPT search indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Applebot&lt;/td&gt;
&lt;td&gt;268&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;Apple (Siri / Apple Intelligence)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClaudeBot&lt;/td&gt;
&lt;td&gt;214&lt;/td&gt;
&lt;td&gt;6.2&lt;/td&gt;
&lt;td&gt;Anthropic training crawler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazonbot&lt;/td&gt;
&lt;td&gt;136&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;Amazon (Alexa &amp;amp; co.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PerplexityBot&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;Perplexity indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things in that table genuinely surprised me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT-User outreads Googlebot.&lt;/strong&gt; 40.3 fetches a day versus 35.8. This isn't a crawler building an index for later — &lt;code&gt;ChatGPT-User&lt;/code&gt; is the user-agent OpenAI sends when a human is &lt;em&gt;mid-conversation&lt;/em&gt; and ChatGPT decides to pull a live page to answer them. On my site, that now happens more often than Googlebot visits. For a tiny business site in a niche market, I did not expect that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bing crawls 4.4x harder than Google.&lt;/strong&gt; 158 requests a day versus 36. Nobody optimizes for Bing. Bing's referral traffic is a rounding error. But &lt;a href="https://www.seerinteractive.com/insights/87-percent-of-searchgpt-citations-match-bings-top-results" rel="noopener noreferrer"&gt;Seer Interactive found that 87% of SearchGPT's citations match Bing's top organic results&lt;/a&gt; (versus 56% for Google) — Bing's index is a structural input to what ChatGPT can retrieve and cite. Suddenly that 4.4:1 ratio reads very differently: the "irrelevant" search engine is the supply chain for the assistant that's actually fetching my pages 40 times a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live retrieval is the biggest AI bucket that matters.&lt;/strong&gt; Grouping by &lt;em&gt;meaning&lt;/em&gt; rather than by bot name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔴 Live user retrieval (a human asked right now)   1,907  (~56/day)
🔎 AI-search indexing (OAI-SearchBot, Perplexity,
   bingbot, Applebot)                               6,096  (~177/day)
📚 Training crawls (GPTBot, ClaudeBot, Amazonbot)     897  (~26/day)
🌐 Classic Google                                   1,233  (~36/day)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,907 live retrievals in 34 days. Each one of those is a moment where a real person asked an AI assistant something, and the assistant chose &lt;em&gt;my&lt;/em&gt; site as a source. That's the leading indicator for citations — and it's the number I now track weekly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they actually read (this is the useful part)
&lt;/h2&gt;

&lt;p&gt;Per-bot top pages turned out to be more interesting than the totals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT-User&lt;/strong&gt; loves the homepage (115 fetches), a WhatsApp automation guide (108), and — third place — &lt;a href="https://achiya-automation.com/en/blog/whatsapp-bot-pricing-guide-2026/" rel="noopener noreferrer"&gt;our WhatsApp bot pricing guide&lt;/a&gt;. People are asking ChatGPT what a WhatsApp bot costs, and ChatGPT is pulling my pricing page to answer them. That's a sales conversation I'm part of without knowing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude-User&lt;/strong&gt; is obsessed with exactly one post: a spam-detection write-up got 233 of its 519 total fetches — 45% of everything Claude reads on my site is one article. One strong, specific technical post apparently becomes a go-to source for one assistant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPTBot&lt;/strong&gt; (the &lt;em&gt;training&lt;/em&gt; crawler) spends its time on &lt;code&gt;/signin&lt;/code&gt; and &lt;code&gt;/forgot-password&lt;/code&gt;. Twelve visits to my login page. Enjoy the training data, I guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perplexity-User: zero.&lt;/strong&gt; PerplexityBot indexes me (103 requests), but not a single live user-triggered retrieval in 34 days. Every assistant has its own taste in sources, and Perplexity's taste apparently doesn't include me. Yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Methodology, and the trap that almost ruined the data
&lt;/h2&gt;

&lt;p&gt;The analyzer is ~60 lines of Python that runs over Caddy's JSON access logs. The core is just user-agent patterns with one ordering rule — the &lt;code&gt;*-User&lt;/code&gt; patterns must be matched &lt;em&gt;before&lt;/em&gt; the generic bot patterns, because the loop stops at first match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BOTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ChatGPT-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ChatGPT-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# live retrieval
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;# live retrieval
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Perplexity-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Perplexity-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OAI-SearchBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OAI-SearchBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GPTBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GPTBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# training
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PerplexityBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Perplexity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# ... bingbot, Googlebot, ClaudeBot, Applebot, Amazonbot
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap: &lt;strong&gt;count only HTTP 200s.&lt;/strong&gt; My first version counted every request, and the numbers were inflated with garbage — security scanners spoof OpenAI user-agents (a scanner claiming to be GPTBot while probing for &lt;code&gt;/wp-admin&lt;/code&gt; on a site that doesn't run WordPress) and land on 404s. Filtering to 200-only removed the impostors and left actual content fetches. If you run this on your own logs and skip that filter, your "AI traffic" will be part vulnerability scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your analytics can't see any of this
&lt;/h2&gt;

&lt;p&gt;None of these 8,900 AI-related requests appear in GA4. No JavaScript execution, no consent banner click, no pageview event. And it's worse than that: on my site, GA4 undercounts &lt;em&gt;humans&lt;/em&gt; by roughly 9x too — only ~11% of visitors accept the cookie banner (I measured this two independent ways over 11 days: ~780 real browser visitors in the logs and widget-loads, 89 in GA4).&lt;/p&gt;

&lt;p&gt;So the fastest-growing reader of my site — AI assistants answering buyer questions in real time — is completely invisible unless you read server logs. If your hosting gives you raw access logs, that's where this story lives. If it doesn't, you can't see it at all, and you might be making content decisions while blind to a quarter of your real readership.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm doing differently now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Watching &lt;code&gt;*-User&lt;/code&gt; agents weekly&lt;/strong&gt;, not rankings daily. Live retrievals are the citation pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Taking Bing seriously for the first time in my life.&lt;/strong&gt; Bing Webmaster Tools went from "never opened it" to the top of this week's list — if 87% of ChatGPT citations flow through Bing's index, it's the cheapest GEO lever there is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing more single-topic, specific pages.&lt;/strong&gt; Claude's 233-fetch obsession with one spam-detection post convinced me: assistants latch onto pages that answer one question thoroughly, not pillar pages that answer twelve questions shallowly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Now I want your denominator.&lt;/strong&gt; Pull one month of access logs and count &lt;code&gt;bingbot&lt;/code&gt; vs &lt;code&gt;Googlebot&lt;/code&gt; hits — my ratio is 4.4:1. If you run a small site, especially a non-English one, I'd genuinely like to know whether that ratio holds outside my niche: drop your two numbers (and your site's language) in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I said my tool supported multiple instances. A stranger running 15 of them filed three bugs. All three were right.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Thu, 13 Aug 2026 08:20:51 +0000</pubDate>
      <link>https://dev.to/achiya-automation/i-said-my-tool-supported-multiple-instances-a-stranger-running-15-of-them-filed-three-bugs-all-3egi</link>
      <guid>https://dev.to/achiya-automation/i-said-my-tool-supported-multiple-instances-a-stranger-running-15-of-them-filed-three-bugs-all-3egi</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;safari-mcp&lt;/a&gt;, an MCP server that lets AI agents drive a real Safari session on macOS. Over a few releases this summer I shipped per-session tab isolation — the feature that lets several agents share one browser without stealing each other's tabs. I wrote about it here. Twice. I was reasonably proud of it.&lt;/p&gt;

&lt;p&gt;Then a stranger showed up with a deployment I had never imagined — three macOS profiles, up to fifteen concurrent server instances — and filed three bug reports in the space of seven seconds.&lt;/p&gt;

&lt;p&gt;All three were right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reports
&lt;/h2&gt;

&lt;p&gt;What made them remarkable wasn't just the hit rate. It was the form. Each issue had a description of the deployment, a repro, and observed data from their own machines. No speculation, no "it feels flaky." They had read the code, formed a hypothesis about where it breaks at their scale, and then watched it break exactly there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: the memory guard that guarded nothing.&lt;/strong&gt; The server has a WebKit memory monitor that sweeps idle tabs before Safari's content process balloons. It also has a gate: only the instance acting as "extension host" — in practice, whichever instance won the race for port 9224 — was allowed to sweep, and only its own tabs. In a fifteen-instance deployment, that means fourteen instances accumulate tabs forever and one instance politely cleans up after itself. The guard was effectively inert precisely in the topology where memory pressure is worst.&lt;/p&gt;

&lt;p&gt;The embarrassing part: the gate was redundant. There was already a lock file guaranteeing one sweeper per cycle. Someone (me) had added a second safety mechanism on top of an existing one, and the second one broke the feature. The fix was a deletion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: the ownership file that ate its own entries.&lt;/strong&gt; Instances record which tabs they own in a shared JSON file. The save path was read-at-startup, modify-in-memory, overwrite-on-save. Classic. With one instance, harmless. With fifteen, every save was a snapshot overwrite that silently dropped whatever the other fourteen had written in the meantime.&lt;/p&gt;

&lt;p&gt;Note what this bug &lt;em&gt;doesn't&lt;/em&gt; do: it doesn't crash, it doesn't touch the wrong tab, it doesn't log anything. Losing an ownership entry is fail-safe — a tab just stops being tracked. Which is exactly why it survived. Bugs that fail loudly get fixed in week one. Bugs that fail &lt;em&gt;politely&lt;/em&gt; wait for someone to run fifteen instances and notice entries vanishing.&lt;/p&gt;

&lt;p&gt;The fix was merge-on-write: union the disk state with local state, newest timestamp wins, with explicit removal deltas so deletions don't get resurrected by a stale peer. I considered a file lock and deliberately skipped it — the residual race window is sub-millisecond and fail-safe, and a lock adds a failure mode of its own. That tradeoff is written down in the code, next to the merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3: the poll that never backed off.&lt;/strong&gt; When a profile's Safari window is closed, the server polls for it to reappear — a fixed 3-second interval spawning an osascript subprocess each time. Forever. Their logs showed the consequence: roughly 1,200 log lines and 1,200 subprocess spawns per hour, per idle instance. Multiply by fifteen.&lt;/p&gt;

&lt;p&gt;The fix is the boring one you already know: self-scheduling backoff, 3s doubling to a 60s cap, reset on success. While fixing it I found a bonus bug the report hadn't mentioned — a trace log being written to &lt;code&gt;__dirname&lt;/code&gt;, which for an npm package means &lt;em&gt;inside node_modules&lt;/em&gt;. That file had been quietly growing inside an installed package directory the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your test topology is a claim about the world, and nobody peer-reviews it.&lt;/strong&gt; I had tested multiple instances — on one profile, launched from one place, in small numbers. The reporter ran three profiles times five. Every one of the three bugs lived precisely in the gap between my topology and theirs. Not one was visible in mine. The tests were green the entire time; the suite passed 78/78 the morning the reports came in. Green tests verify the world you thought to simulate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail-safe failures are the long-lived ones.&lt;/strong&gt; All three bugs share a property: the system kept working. Memory got worse slowly. Entries vanished silently. Subprocesses spawned invisibly. If any of them had thrown an exception, they'd have died in code review. Design your failures to be loud, or accept that the quiet ones will be found by a stranger, years later, at a scale you didn't test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sometimes the fix is a deletion.&lt;/strong&gt; The memory-guard bug existed because two safety mechanisms overlapped and the newer one strangled the feature. I didn't need to write cleverer coordination code — I needed to remove the redundant half. The diff that fixed the scariest-sounding of the three bugs was net negative lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A good bug report at foreign scale is free QA you could not have bought.&lt;/strong&gt; I could not have afforded to stand up a fifteen-instance, three-profile deployment as a test rig for a feature I wasn't sure anyone used that hard. Someone built it because they needed it, then handed me the findings with repros. The only correct responses are fast verification, honest credit, and shipping the fix. All three fixes went out in one release the next day, each issue closed with a reply documenting not just what changed but which alternatives I rejected and why — including the lock I chose not to add.&lt;/p&gt;

&lt;p&gt;That last part matters more than it looks. The reporter now knows the merge-on-write has a known, bounded, fail-safe race. The next person who hits something weird there won't start from zero — the reasoning is in the thread, not in my head.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable question
&lt;/h2&gt;

&lt;p&gt;The feature worked. The story I told about it was, at my scale, true. It took someone else's production topology to reveal that "supports multiple instances" was really "supports the multiple-instance shapes I happened to try."&lt;/p&gt;

&lt;p&gt;What's the biggest gap between your test topology and a user's real deployment that you've been bitten by — and did the bug fail loudly or politely?&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>An invisible character broke a security patch. Then it broke my review. Then it broke my review of the fix.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Sun, 09 Aug 2026 07:15:27 +0000</pubDate>
      <link>https://dev.to/achiya-automation/an-invisible-character-broke-a-security-patch-then-it-broke-my-review-then-it-broke-my-review-of-444c</link>
      <guid>https://dev.to/achiya-automation/an-invisible-character-broke-a-security-patch-then-it-broke-my-review-then-it-broke-my-review-of-444c</guid>
      <description>&lt;p&gt;My open-source project, &lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;safari-mcp&lt;/a&gt;, got a security-hardening pull request from an automated scanner. The idea behind the patch was right. The patch itself un-parsed the codebase. Then the same defect infected my review of the patch, my review of the fix, and eventually the private note I wrote to remind myself to stop letting this happen.&lt;/p&gt;

&lt;p&gt;This is a story about one character, told in three acts, and it ends with a rule I now apply to everything I publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The character
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;U+2028 LINE SEPARATOR&lt;/code&gt; (and its sibling &lt;code&gt;U+2029 PARAGRAPH SEPARATOR&lt;/code&gt;) is a legal Unicode character that most terminals and editors render as a space, or as nothing at all.&lt;/p&gt;

&lt;p&gt;In JavaScript it has two sharp edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In strings&lt;/strong&gt;, it counts as a line terminator. If you build a single-quoted string by escaping only quotes and backslashes, a raw &lt;code&gt;U+2028&lt;/code&gt; in the payload can terminate the literal early. That is a classic injection vector, and it is what the pull request set out to fix. Legitimate concern, real hardening value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In regex literals&lt;/strong&gt;, the grammar forbids line terminators outright. A raw &lt;code&gt;U+2028&lt;/code&gt; between the slashes of &lt;code&gt;/.../&lt;/code&gt; is not a weird regex. It is a &lt;code&gt;SyntaxError&lt;/code&gt; at parse time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep both edges in mind. The first one motivated the patch. The second one is what the patch shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Act 1: the patch that un-parsed the codebase
&lt;/h2&gt;

&lt;p&gt;The PR extended my escaping helper to also escape &lt;code&gt;\r&lt;/code&gt;, &lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;U+2028&lt;/code&gt; and &lt;code&gt;U+2029&lt;/code&gt;. Seven lines added, one changed. The problem: the new &lt;code&gt;.replace()&lt;/code&gt; calls embedded the &lt;strong&gt;raw characters&lt;/strong&gt; inside regex literals, instead of the escape sequences &lt;code&gt;\u2028&lt;/code&gt; and &lt;code&gt;\u2029&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My terminal renders &lt;code&gt;U+2028&lt;/code&gt; as a space. So the diff &lt;em&gt;looked&lt;/em&gt; like it said &lt;code&gt;.replace(/ /g, ...)&lt;/code&gt; — replace every space. That would be a different catastrophic bug. The truth was worse in a more interesting way: the file no longer parsed at all. &lt;code&gt;node --check&lt;/code&gt; failed with a &lt;code&gt;SyntaxError&lt;/code&gt;. The main server file imports that module, so the entire MCP server failed to start.&lt;/p&gt;

&lt;p&gt;A security patch that prevents the server from running is, to be fair, extremely secure.&lt;/p&gt;

&lt;p&gt;What caught it was not reading the code. It was refusing to trust rendering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python &lt;code&gt;repr()&lt;/code&gt; on the raw diff showed the literal characters.&lt;/li&gt;
&lt;li&gt;A fun tell: Python's &lt;code&gt;splitlines()&lt;/code&gt; splits on &lt;code&gt;U+2028&lt;/code&gt;, so a "one-line" regex was quietly two lines to Python.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node --check&lt;/code&gt; turned suspicion into proof.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote all of that up in a &lt;code&gt;REQUEST_CHANGES&lt;/code&gt; review, with a corrected snippet showing the right approach: put the ASCII escape sequence &lt;code&gt;\u2028&lt;/code&gt; in the source, never the raw character.&lt;/p&gt;

&lt;h2&gt;
  
  
  Act 2: my review had the same bug
&lt;/h2&gt;

&lt;p&gt;I composed that review body in a shell heredoc. Heredocs pass Unicode through byte-for-byte. The corrected snippet I posted — the one teaching the contributor to use escape sequences — was posted &lt;strong&gt;with the raw characters in it&lt;/strong&gt;, exactly where the ASCII text &lt;code&gt;\u2028&lt;/code&gt; was supposed to appear.&lt;/p&gt;

&lt;p&gt;The review explaining the invisible-character bug contained the invisible-character bug.&lt;/p&gt;

&lt;p&gt;I only caught it because, after posting, I fetched my own review body back through the API and ran &lt;code&gt;repr()&lt;/code&gt; on it. Then I rebuilt the body in Python, constructing the escape-sequence text from character codes instead of typing it, and patched the review via the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Act 3: the fix was perfect. My approval was not.
&lt;/h2&gt;

&lt;p&gt;The contributor fixed everything properly: escape sequences in the regex literals, a regression test that locks the full escaping recipe (backslash, quote, &lt;code&gt;\r&lt;/code&gt;, &lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;U+2028&lt;/code&gt;, &lt;code&gt;U+2029&lt;/code&gt; — in that order, so order drift fails the suite), and a documented behavior change. I verified it in a fresh clone at byte level. Full test suite green. I approved.&lt;/p&gt;

&lt;p&gt;My approval review quoted those escape sequences. It posted with raw &lt;code&gt;U+2028&lt;/code&gt; in it. Again. Same failure, same detection (re-fetch, &lt;code&gt;repr()&lt;/code&gt;), same API patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Epilogue: it kept going
&lt;/h2&gt;

&lt;p&gt;The status row I wrote about the incident, in my own tracking file? Raw &lt;code&gt;U+2028&lt;/code&gt; in it.&lt;/p&gt;

&lt;p&gt;The personal rule file I then wrote — literally titled "byte-verify anything that discusses this character" — was written to disk &lt;strong&gt;with two raw &lt;code&gt;U+2028&lt;/code&gt; bytes in it&lt;/strong&gt;. The note about the disease was a carrier.&lt;/p&gt;

&lt;p&gt;Four artifacts in two days. Every one of them was &lt;em&gt;about&lt;/em&gt; the defect. Every one of them contained it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this keeps happening
&lt;/h2&gt;

&lt;p&gt;Text that discusses a character tends to contain that character. When I write about &lt;code&gt;U+2028&lt;/code&gt;, some layer in the pipeline — text generation, a heredoc, an editor buffer, a clipboard — will happily emit the real code point where I intended the six ASCII characters that name it. Every layer treats it as ordinary printable Unicode and passes it through. Every renderer displays it as nothing.&lt;/p&gt;

&lt;p&gt;So there is no natural checkpoint where the defect becomes visible. The character is only "visible" in a byte-level representation: &lt;code&gt;repr()&lt;/code&gt;, a hex dump, an assertion. If your verification is "I looked at it and it looked fine," you have verified nothing — for this class of bug, &lt;em&gt;looking&lt;/em&gt; is the one sense that cannot detect it.&lt;/p&gt;

&lt;p&gt;It has a quine-like quality that I find genuinely funny now, several patches later: the document about the virus carries the virus.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that survived
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never trust rendering for invisible characters.&lt;/strong&gt; Verify bytes: &lt;code&gt;repr()&lt;/code&gt; in Python, &lt;code&gt;xxd&lt;/code&gt;, &lt;code&gt;grep -P&lt;/code&gt; with the code point. Eyes are the wrong instrument.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose text about escape characters programmatically.&lt;/strong&gt; I now build the string "\u2028" as &lt;code&gt;chr(92) + "u2028"&lt;/code&gt; in a script. I do not type it into anything that will be published.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert before posting.&lt;/strong&gt; &lt;code&gt;assert chr(0x2028) not in body&lt;/code&gt; costs one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-fetch after posting and verify the published artifact.&lt;/strong&gt; The pipeline between you and the platform can preserve what you thought you had stripped. Verify what readers actually see, not what you sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own artifacts are in scope.&lt;/strong&gt; Byte-checking a stranger's diff is the obvious part. The review of the diff, the changelog entry, the tracking note, the article — those are artifacts too, and they are &lt;em&gt;more&lt;/em&gt; likely to carry the character, because they talk about it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The meta part
&lt;/h2&gt;

&lt;p&gt;This article discusses &lt;code&gt;U+2028&lt;/code&gt; roughly thirty times. It was generated by a script that constructs every single mention from character codes and asserts that the raw bytes are absent before the file is written. After publishing, I fetched the live body back and byte-verified it.&lt;/p&gt;

&lt;p&gt;Based on my track record of the last two days: if I had skipped any of that, you would right now be reading an article about an invisible character, carrying the invisible character.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the nastiest invisible-character bug you have hit — and did your tooling show it to you, or did you find it by accident?&lt;/strong&gt; Zero-width joiners in usernames, BOMs at the top of config files, non-breaking spaces in YAML — I want to hear the war stories.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I gave a user the right answer with the wrong mechanism. They caught it within a day.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Tue, 04 Aug 2026 08:17:36 +0000</pubDate>
      <link>https://dev.to/achiya-automation/i-gave-a-user-the-right-answer-with-the-wrong-mechanism-he-caught-it-within-a-day-2k41</link>
      <guid>https://dev.to/achiya-automation/i-gave-a-user-the-right-answer-with-the-wrong-mechanism-he-caught-it-within-a-day-2k41</guid>
      <description>&lt;p&gt;Last week I wrote about &lt;a href="https://dev.to/achiya-automation/a-user-spent-four-days-designing-a-feature-for-my-project-the-right-answer-was-zero-lines-of-code-bff"&gt;saying no to a feature request&lt;/a&gt;: a user designed a &lt;code&gt;laneId&lt;/code&gt; mode for my project over four days, and the right answer turned out to be zero lines of code, because they ended up shipping the fix themselves — in their own layer.&lt;/p&gt;

&lt;p&gt;That post was accurate. The decision was right. The code that shipped was right.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;explanation&lt;/em&gt; I gave them for why a particular setup would work — the one I wrote with full confidence, in public, on the issue thread — was wrong. Not completely wrong. Wrong in the more embarrassing way: one layer off.&lt;/p&gt;

&lt;p&gt;They caught it within a day of my closing reply.&lt;/p&gt;

&lt;h2&gt;
  
  
  The claim
&lt;/h2&gt;

&lt;p&gt;Quick context: I maintain &lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;safari-mcp&lt;/a&gt;, an MCP server that gives AI agents a real Safari session on macOS. The whole thread (&lt;a href="https://github.com/achiya-automation/safari-mcp/issues/76" rel="noopener noreferrer"&gt;#76&lt;/a&gt;) was about running &lt;em&gt;multiple&lt;/em&gt; agents against one Safari without them stealing each other's tabs.&lt;/p&gt;

&lt;p&gt;My server already had the hard part: in HTTP daemon mode, every client gets its own &lt;code&gt;Mcp-Session-Id&lt;/code&gt;, and the server keys all tab state — active tab, ownership markers, the works — off that session. Two clients on one daemon cannot see each other's tab pointer. I had shipped a real fix for a real bug in that mechanism days earlier. Tests, verification, release notes. That layer was solid, and I knew it was solid.&lt;/p&gt;

&lt;p&gt;The user runs their agents through &lt;a href="https://github.com/openclaw/mcporter" rel="noopener noreferrer"&gt;mcporter&lt;/a&gt;, a runner that manages MCP servers for multiple agent sessions. They'd found mcporter's own isolation knobs — a separate daemon directory per agent — and asked whether that would cover them.&lt;/p&gt;

&lt;p&gt;Here's what I told them, nearly verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Separate daemon dir per agent + stdio transport = isolation by process boundary, but you're back to N Safari-controller processes.&lt;br&gt;
Separate daemon dir per agent + &lt;strong&gt;HTTP transport = the winning combo&lt;/strong&gt; — each mcporter daemon holds its own MCP client, so each gets its own &lt;code&gt;Mcp-Session-Id&lt;/code&gt;, so my per-session isolation engages. One process, full isolation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Clean analysis. Two topologies, one recommendation, a confident bolded phrase. I framed their lane concern as something you only need on the stdio path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The correction
&lt;/h2&gt;

&lt;p&gt;They built and published their fix (&lt;a href="https://pi.dev/packages/mcporter-lanes" rel="noopener noreferrer"&gt;mcporter-lanes&lt;/a&gt;), I credited it, updated my README, wrote the blog post, closed the loop. Then, about an hour after my closing reply, they posted again — politely, precisely:&lt;/p&gt;

&lt;p&gt;With mcporter in the middle, lanes are needed &lt;strong&gt;regardless of transport&lt;/strong&gt;. Because mcporter does one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Caches one MCP client and reuses it for every agent session.&lt;/strong&gt; My server sees exactly one &lt;code&gt;Mcp-Session-Id&lt;/code&gt;. My beautiful per-session isolation never engages — not because it's broken, but because from where my server stands, there &lt;em&gt;is&lt;/em&gt; only one session. Every agent walks in wearing the same badge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creates a fresh client per call.&lt;/strong&gt; Now there's no session continuity at all — each tool call is a stranger, and per-session state is meaningless in the other direction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no middle ground between those two without lanes. The collapse doesn't live in the transport choice. It lives in mcporter's client cache — one layer &lt;em&gt;upstream&lt;/em&gt; of everything I had analyzed.&lt;/p&gt;

&lt;p&gt;My "winning combo" framing wasn't a winning combo. Transport was never the variable that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I got it wrong
&lt;/h2&gt;

&lt;p&gt;I want to be precise about the failure, because it's a failure mode I suspect most maintainers share.&lt;/p&gt;

&lt;p&gt;I verified my own layer exhaustively. I read my session map code before answering. I had literally shipped and tested the per-session mechanism that same week. Every claim I made about &lt;em&gt;my server&lt;/em&gt; was true.&lt;/p&gt;

&lt;p&gt;But "each client gets its own session" carries a silent assumption: &lt;strong&gt;that each agent is a distinct client.&lt;/strong&gt; Whether that's true is decided by the middleware between the agent and my server — a layer I don't own, don't ship, and hadn't read. I reasoned about mcporter's topology by analogy ("a daemon per agent surely means a client per agent") instead of by its actual caching behavior.&lt;/p&gt;

&lt;p&gt;The guarantee I was advertising was a chain: agent → runner → client cache → transport → my server → browser extension. I had audited exactly one link and pronounced the chain sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix was one sentence
&lt;/h2&gt;

&lt;p&gt;Here's the part that keeps this in the same family as the last post: the correction required zero lines of code, again.&lt;/p&gt;

&lt;p&gt;My server's behavior was correct in both topologies. What was wrong was a paragraph in my README that framed lanes as a stdio-only concern. The fix (commit &lt;code&gt;f12e5e3&lt;/code&gt;) rewords it: if you're behind mcporter, you need lanes &lt;em&gt;on any transport&lt;/em&gt;, because the client cache sits upstream of the transport choice.&lt;/p&gt;

&lt;p&gt;One sentence. But a load-bearing sentence — it's the difference between the next user with this setup debugging a "broken" isolation feature for an afternoon, and them installing the right package in two minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm taking from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your isolation guarantee is only as strong as the weakest layer between the user and you.&lt;/strong&gt; And most of those layers aren't yours. Documenting a guarantee without naming its assumptions ("this engages &lt;em&gt;if&lt;/em&gt; each agent presents its own session") is how correct code produces incorrect promises.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public reasoning gets corrected while it's still cheap.&lt;/strong&gt; I posted my topology analysis on the issue instead of just DMing a recommendation. That's the only reason the wrong mechanism lived for about a day instead of shipping into folklore. The user could quote my exact sentence back at me and point at the exact layer where it broke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I verified my layer" feels identical to "I verified the system."&lt;/strong&gt; From the inside, the two are indistinguishable — both come with the same warm confidence. The only reliable tiebreaker I know is someone who actually runs the other layers. They had read mcporter's internals. I had read mine. Together we had read the system; separately, neither of us had.&lt;/p&gt;

&lt;p&gt;I closed my last post saying the user solved their problem in the layer where it actually lived. Turns out my explanation had the same bug as the feature request: right idea, wrong layer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you ever documented a guarantee your own layer couldn't actually promise — one that depended on middleware you don't control? How did you find out?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>A user spent four days designing a feature for my project. The right answer was zero lines of code.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:12:44 +0000</pubDate>
      <link>https://dev.to/achiya-automation/a-user-spent-four-days-designing-a-feature-for-my-project-the-right-answer-was-zero-lines-of-code-bff</link>
      <guid>https://dev.to/achiya-automation/a-user-spent-four-days-designing-a-feature-for-my-project-the-right-answer-was-zero-lines-of-code-bff</guid>
      <description>&lt;p&gt;Last week I got the best feature request my open-source project has ever received. It was detailed, technically literate, fail-closed by design, and came from someone who clearly runs the tool harder than I do.&lt;/p&gt;

&lt;p&gt;I'm not going to build it. And the person who asked for it agrees — because the conversation ended with &lt;em&gt;him&lt;/em&gt; shipping the fix, in his own layer, as a package other people can now use.&lt;/p&gt;

&lt;p&gt;I want to walk through how that happened, because the whole arc is a case study in a question I think we ask too rarely: not "how do I implement this?" but "&lt;strong&gt;where does this concern actually live?&lt;/strong&gt;"&lt;/p&gt;

&lt;h2&gt;
  
  
  The request
&lt;/h2&gt;

&lt;p&gt;My project is &lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;safari-mcp&lt;/a&gt;, an MCP server that lets AI coding agents drive the real Safari on macOS — your session, your logins, your tabs. Which is exactly what makes concurrency dangerous: if two agents share one browser, "the active tab" is a loaded gun.&lt;/p&gt;

&lt;p&gt;The issue, from a user running parallel AI agent sessions against his everyday Safari, asked for &lt;strong&gt;isolated tab lanes&lt;/strong&gt;: each concurrent client gets stable ownership of its own tab, explicit targeting, and — his words, and the best phrase in the whole thread — behavior that &lt;em&gt;never falls back to whatever tab is in front&lt;/em&gt;, because the front tab is usually the human's.&lt;/p&gt;

&lt;p&gt;Five concrete asks. A design sketch. Fail-closed semantics specified up front. Feature requests do not get better than this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise one: most of it already existed
&lt;/h2&gt;

&lt;p&gt;My first instinct was to answer from the roadmap. I made myself read the source instead, and the result was uncomfortable: &lt;strong&gt;three of his five asks had already shipped.&lt;/strong&gt; Per-session tab state keyed by MCP session id. Tab identity by a marker stamped into the page, not by index, so it survives navigation and tab reordering. And the fail-closed refusal he asked for existed &lt;em&gt;nearly verbatim&lt;/em&gt; — the codebase literally prints "refusing to fall back to 'current tab of window' (would target the user's active tab)."&lt;/p&gt;

&lt;p&gt;He's a competent engineer who read the README carefully and reasonably concluded none of this existed. That's not his failure. All of it was documented only in a design doc the README never linked. A capability nobody can discover is indistinguishable from a capability you never built — so the first "feature" I shipped from this thread was a README section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise two: the feature request found a real bug
&lt;/h2&gt;

&lt;p&gt;Then his follow-up made it better. He dug into &lt;em&gt;why&lt;/em&gt; isolation wasn't engaging in his setup and found that in shared-daemon mode, every client's browser-extension traffic carried the same process-wide session id — so the extension saw all clients as one session, and one client could be served another's cached tab.&lt;/p&gt;

&lt;p&gt;He was right. I verified it in the source, shipped the fix the same day, and his name is in the release notes. A feature request that produces a shipped bugfix has already paid for itself. But the main course was still on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proposal I liked and still didn't build
&lt;/h2&gt;

&lt;p&gt;His remaining gap was real: his client tooling multiplexes several agent sessions through one connection pool, collapsing them into a &lt;em&gt;single&lt;/em&gt; MCP client — so per-session isolation, which keys on the client boundary, never gets a chance to engage. His proposal: a &lt;code&gt;laneId&lt;/code&gt; parameter on every tool call, required, fail-closed, routed into the existing session map.&lt;/p&gt;

&lt;p&gt;Here's the trap: internally, this was &lt;em&gt;almost free&lt;/em&gt;. The per-session state machinery already existed; running each call in a lane-scoped context would have reused it nearly unchanged. When a feature is one afternoon away, "cheap to build" starts masquerading as "right to build."&lt;/p&gt;

&lt;p&gt;But the cost wasn't in the implementation. The server exposes 97 tools, and a required &lt;code&gt;laneId&lt;/code&gt; means &lt;strong&gt;97 tool schemas grow a parameter&lt;/strong&gt; — every schema an agent reads, every byte of context it burns, in every single-client setup that will never have two clients. A multi-client concern would leak into everyone's single-client API, forever. Schemas are the one part of an MCP server you can't refactor quietly later.&lt;/p&gt;

&lt;p&gt;So I did the annoying thing: I said I liked it, wrote down exactly how it &lt;em&gt;would&lt;/em&gt; be built and gated if ever needed — and held the build. Publicly. In the issue. Which felt like stalling, right up until it turned out to be the move that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the concern actually lived
&lt;/h2&gt;

&lt;p&gt;Look at the shape of the problem again. The isolation wasn't missing from my server — it was being &lt;em&gt;erased upstream&lt;/em&gt;, by the layer that collapsed N agent sessions into one client. Asking my server to add lanes was asking the downstream layer to compensate for something the upstream layer destroyed.&lt;/p&gt;

&lt;p&gt;He reached the same conclusion, went looking in his own stack — and found his multiplexer already had per-instance isolation knobs. Then he did the genuinely great thing: instead of a private workaround, he &lt;strong&gt;built and published an extension&lt;/strong&gt; that gives each of his agent sessions its own daemon directory — its own connection, its own server process, its own lane — with a composite lane key for the concurrency edge cases and an idle timeout so processes don't pile up.&lt;/p&gt;

&lt;p&gt;It's not specific to my project. It works for &lt;em&gt;any&lt;/em&gt; stateful MCP server behind that multiplexer. The fix landed in the layer that owned the problem, and it landed as infrastructure other people can reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard
&lt;/h2&gt;

&lt;p&gt;Final tally for the "rejected" feature request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A README section that made existing isolation discoverable, instead of documented-but-invisible&lt;/li&gt;
&lt;li&gt;A real cross-client session bug found, verified, and shipped as a fix — credited to the reporter&lt;/li&gt;
&lt;li&gt;A reusable extension, published by the user, solving the problem in the layer that owned it&lt;/li&gt;
&lt;li&gt;A written-down design for &lt;code&gt;laneId&lt;/code&gt;, on the record, buildable in a day if the need ever materializes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero lines of feature code in my repo&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YAGNI usually gets framed as refusal — a door closed in the requester's face. Done right, it's redirection: the effort didn't disappear, it moved to where it belonged, and the ecosystem ended up with more than my feature would have delivered. The best code review I ever got was a feature request. The best feature I shipped last week was a link to someone else's package.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the best feature request you've ever declined — and did saying "not here" ever produce something better than building it would have? I'd genuinely like to hear the counter-examples too: the time you held the line on YAGNI and it turned out you did need it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a guard that refused to read the user's tab. Then my own cleanup code closed it.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Mon, 27 Jul 2026 06:40:34 +0000</pubDate>
      <link>https://dev.to/achiya-automation/i-built-a-guard-that-refused-to-read-the-users-tab-then-my-own-cleanup-code-closed-it-3fpe</link>
      <guid>https://dev.to/achiya-automation/i-built-a-guard-that-refused-to-read-the-users-tab-then-my-own-cleanup-code-closed-it-3fpe</guid>
      <description>&lt;p&gt;Three days ago my browser automation tool closed one of my own tabs. Not a tab it had opened — a dashboard I had open in another window, with a page I hadn't finished reading.&lt;/p&gt;

&lt;p&gt;What makes it worth writing up isn't the bug. It's that the guard designed to prevent exactly this had already fired, correctly, ninety seconds earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard worked
&lt;/h2&gt;

&lt;p&gt;Safari MCP lets an AI agent drive your real, logged-in Safari. That premise means the single worst thing it can do is act on a tab you're using. So there's an identity system: every tab the tool opens gets a marker stamped into &lt;code&gt;window.name&lt;/code&gt;, which survives navigation, redirects, and cross-origin loads. Before running anything in a tab, the tool checks the marker.&lt;/p&gt;

&lt;p&gt;I was filling in a form. The URL was a &lt;code&gt;forms.gle&lt;/code&gt; shortlink, which 302s to &lt;code&gt;docs.google.com&lt;/code&gt; — a cross-origin redirect that, it turns out, drops &lt;code&gt;window.name&lt;/code&gt;. My next read came back refused:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tab tracking lost — refusing to target the user's current tab.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Correct. Exactly the intended behaviour. The tool no longer knew which tab was its own, so it declined to guess.&lt;/p&gt;

&lt;p&gt;So I did the tidy thing and cleaned up my orphaned tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;safari_close_tab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It closed a different tab. One of mine. The tool went from &lt;em&gt;"I can't prove which tab is mine, so I won't read"&lt;/em&gt; to &lt;em&gt;"let me close a tab"&lt;/em&gt; in one step, and nobody stopped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the hole
&lt;/h2&gt;

&lt;p&gt;Here is the close path as it existed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;osascript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`... close tab &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;osascript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`... close current tab of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ← the user's tab&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;current tab of window&lt;/code&gt; is whatever the user is looking at. So the fallback for &lt;em&gt;"I don't know which tab is mine"&lt;/em&gt; was &lt;em&gt;"close theirs."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That branch is only reachable when the index is unknown — which is precisely the state the guard had just announced. The two pieces of code were describing the same condition and disagreeing about what it meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers, one mistake
&lt;/h2&gt;

&lt;p&gt;When I went looking, the same fail-open was in all three layers of the stack, and it had the same shape every time: &lt;strong&gt;no ownership recorded was read as permission, not as refusal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first one is the most embarrassing, because it's a list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;_noOwnershipCheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="c1"&gt;// Tab management&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new_tab&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list_tabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close_tab&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;switch_tab&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A set of operations exempt from the ownership check, grouped under the comment "read-only or tab management". Three of those four are harmless: &lt;code&gt;new_tab&lt;/code&gt; creates a tab, &lt;code&gt;list_tabs&lt;/code&gt; reads, and &lt;code&gt;switch_tab&lt;/code&gt; carries its own ownership check at the tool level. &lt;code&gt;close_tab&lt;/code&gt; destroys a user tab and had no check anywhere.&lt;/p&gt;

&lt;p&gt;It wasn't exempted by argument. It was exempted by &lt;em&gt;category&lt;/em&gt; — it looked like tab management, it sat next to tab management, so it inherited tab management's safety assumptions. Nobody ever wrote down "closing a tab is safe." The list said it, silently, by adjacency.&lt;/p&gt;

&lt;p&gt;The third layer, the browser extension, had a variant worth naming on its own. Its guard refuses an operation when the session owns tabs and this isn't one of them — but when the session owns &lt;em&gt;nothing&lt;/em&gt;, it allows the operation, with this comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// If no tabs owned yet, allow operation (backward compatibility for sessions that don't use new_tab)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That leniency is sensible for a write: worst case it edits the page you're already on. For a close it's a disaster. And "owns nothing" is exactly what a session reports after its transport drops and the client re-initialises — mid-task, tab still open. The lenient branch is reachable &lt;em&gt;only&lt;/em&gt; in the state where it's most wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I actually got wrong
&lt;/h2&gt;

&lt;p&gt;My first instinct for the fix was to reuse the rule the read path already uses: refuse a tab carrying another session's marker, allow an unmarked one. It keeps "read the page I'm looking at" working for a session that genuinely never opened a tab.&lt;/p&gt;

&lt;p&gt;That rule would not have prevented this. The tab I destroyed was a genuine user tab. It had no marker at all. Under "unmarked is fine", it sails straight through to &lt;code&gt;tabs.remove()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which forced the actual distinction: &lt;strong&gt;a wrong read costs information; a wrong close costs the user their work.&lt;/strong&gt; They don't get the same rule. The read paths keep the lenient fallback on purpose. The destructive path gets no fallback whatsoever — it closes a tab it can positively prove it owns, or it throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;explicitIndex&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_provenOwnTabIndex&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`Tab tracking lost — refusing to close a tab this session cannot prove it opened ...`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;else&lt;/code&gt;. "Owns nothing" now means "closes nothing", which is the sentence that should have been in that first list all along.&lt;/p&gt;

&lt;p&gt;A few adjacent things fell out of it. Blanking a window's last tab — the workaround for "closing this would quit Safari" — used the same fallback, and throwing away someone's loaded page is destructive too; it's pinned to the proven index now. And in the extension, the guard validated one tab (&lt;code&gt;tabId&lt;/code&gt;) while the indexed branch removed a &lt;em&gt;different&lt;/em&gt; one (&lt;code&gt;_winTabs[index - 1]&lt;/code&gt;), so the check and the action were pointed at different tabs. That one had never fired in production; it was just waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take from it
&lt;/h2&gt;

&lt;p&gt;The guard wasn't wrong and the fallback wasn't wrong. What was wrong is that they were two different answers to one question — &lt;em&gt;do we know which tab is ours?&lt;/em&gt; — living in two files, and only one of them had thought about what the answer implied.&lt;/p&gt;

&lt;p&gt;I've since stopped trusting the word "safe" in a category name. &lt;code&gt;close_tab&lt;/code&gt; was in a set called tab management, and the set was right: it &lt;em&gt;is&lt;/em&gt; tab management. Categories describe what an operation is. Guards have to be about what it costs when it's wrong. Sorting by the first and inheriting the second is how a destructive call ends up on the exempt list with no one having decided that.&lt;/p&gt;

&lt;p&gt;The test I wrote isn't behavioural, for the same reason as last time: the defect is a &lt;em&gt;missing refusal&lt;/em&gt; on a path that only runs after state is already lost. There's nothing to exercise. It reads the source and asserts that no AppleScript verb in the close path targets the front document — and I checked that it fails when I put the old branch back, because a regression test you haven't seen fail is a guess.&lt;/p&gt;

&lt;p&gt;Fixed in v2.15.8. The repo is &lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;safari-mcp&lt;/a&gt; — the full analysis is in &lt;a href="https://github.com/achiya-automation/safari-mcp/issues/68" rel="noopener noreferrer"&gt;issue #68&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A question for you:&lt;/strong&gt; how do you catch the operations that got their safety assumptions by adjacency — the ones sitting in a list they only half belong to? Every review I've done reads the code in the branch, not the membership of the set above it.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I run an affiliate code. Seven fake ones outrank it — and Google's AI believes them.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:01:27 +0000</pubDate>
      <link>https://dev.to/achiya-automation/i-run-an-affiliate-code-seven-fake-ones-outrank-it-and-googles-ai-believes-them-19h1</link>
      <guid>https://dev.to/achiya-automation/i-run-an-affiliate-code-seven-fake-ones-outrank-it-and-googles-ai-believes-them-19h1</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure up front: I'm a Chatwoot affiliate. The links in this post are affiliate links, and the code I mention earns me a commission. That's exactly what this post is about, so it would be strange to bury it at the bottom.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last week I did something I'd never bothered to do: I searched Google for my own affiliate code.&lt;/p&gt;

&lt;p&gt;Google asked me if I meant "ACHIEVERS."&lt;/p&gt;

&lt;p&gt;Two results came back, both from my own site. That's the entire web footprint of the code. Meanwhile, the first organic result for &lt;code&gt;chatwoot coupon&lt;/code&gt; promises &lt;strong&gt;70% off&lt;/strong&gt;, and a page ranking for &lt;code&gt;chatwoot discount code&lt;/code&gt; confidently tells you the code is &lt;code&gt;START&lt;/code&gt; for 20% off.&lt;/p&gt;

&lt;p&gt;Neither of those can be true, and the reason is arithmetic, not opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ceiling is public
&lt;/h2&gt;

&lt;p&gt;Chatwoot publishes its affiliate terms on its own site:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Refer businesses to Chatwoot Cloud and earn 20% of the revenue for the first 12 months. Your referrals get 5% off their subscription too."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the whole mechanism. A partner earns 20% of the revenue for a year; the person they refer gets &lt;strong&gt;5% off&lt;/strong&gt;. There is no tier that issues 20%, 50% or 70% to a referral, because there's no lever in the program that produces one.&lt;/p&gt;

&lt;p&gt;So when I went looking, here's what's actually circulating as of July 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Claimed discount&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;START&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20% off&lt;/td&gt;
&lt;td&gt;three separate blogs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PINTU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;50% off&lt;/td&gt;
&lt;td&gt;AI-generated article site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;KUMAR&lt;/code&gt; / &lt;code&gt;MALIK&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;60% off&lt;/td&gt;
&lt;td&gt;startup directory, coupon aggregator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SAVE55&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unspecified&lt;/td&gt;
&lt;td&gt;LinkedIn "newsletter"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unspecified&lt;/td&gt;
&lt;td&gt;YouTube short&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;5OFFCODE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;email-tools blog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;"70% off", "$100 off"&lt;/td&gt;
&lt;td&gt;coupon aggregators, no code attached&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only one of those is even in the right ballpark, and it isn't attached to a real referral account as far as I can tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually matters
&lt;/h2&gt;

&lt;p&gt;Here's the thing I didn't expect, and the reason I'm writing this instead of shrugging.&lt;/p&gt;

&lt;p&gt;I've been tracking whether Google's AI Overview names a specific code for these queries. One day it named mine across all three coupon queries I track. The next day — after no change on my end — it named &lt;strong&gt;none of them&lt;/strong&gt;, and instead fell back to citing chatwoot.com for the generic fact that referrals get 5% off.&lt;/p&gt;

&lt;p&gt;It got the &lt;em&gt;fact&lt;/em&gt; right. It just wouldn't commit to a &lt;em&gt;code&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When I looked at the source distribution, the reason was obvious. Every fake code sits on 8+ independent domains. Mine sits on one. An LLM asked "what's the real code" is doing something closer to counting witnesses than checking arithmetic — and by that measure, seven fabrications beat one accurate source.&lt;/p&gt;

&lt;p&gt;This is the uncomfortable version of the "AI search rewards authority" advice everyone repeats. It doesn't reward being &lt;em&gt;right&lt;/em&gt;. It rewards being &lt;em&gt;corroborated&lt;/em&gt;. Those are different things, and coupon spam is the cleanest natural experiment I've seen for the gap between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why coupon spam is structurally good at this
&lt;/h2&gt;

&lt;p&gt;Fake discount content has properties that make it unusually effective against retrieval-based systems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's cheap to generate.&lt;/strong&gt; A "verified 2026 promo code" page is a template. One prompt produces fifty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's unfalsifiable at read time.&lt;/strong&gt; A crawler can't try the code at checkout. Neither can an LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's mutually reinforcing.&lt;/strong&gt; Aggregators scrape each other, so a single fabrication propagates into what looks like independent confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody corrects it.&lt;/strong&gt; The vendor doesn't chase coupon sites, and the affiliate who'd care is one voice against a dozen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The honest actor is structurally disadvantaged. I can say the real number is 5%, but I can only say it once, from one domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did about it
&lt;/h2&gt;

&lt;p&gt;Three things, none of them clever:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Said the number plainly and named the fakes.&lt;/strong&gt; My &lt;a href="https://achiya-automation.com/en/blog/chatwoot-vs-intercom/" rel="noopener noreferrer"&gt;Chatwoot vs Intercom comparison&lt;/a&gt; now lists &lt;code&gt;START&lt;/code&gt;, &lt;code&gt;PINTU&lt;/code&gt;, &lt;code&gt;KUMAR&lt;/code&gt;, &lt;code&gt;MALIK&lt;/code&gt;, &lt;code&gt;SAVE55&lt;/code&gt;, &lt;code&gt;GET50&lt;/code&gt; and &lt;code&gt;5OFFCODE&lt;/code&gt; explicitly and explains why each is inconsistent with the published program terms. Naming them is the part that matters — "there are fake codes out there" is unquotable; "&lt;code&gt;START&lt;/code&gt; doesn't work because the ceiling is 5%" is a sentence a model can lift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quoted the primary source instead of paraphrasing it.&lt;/strong&gt; Chatwoot's own affiliate page is the authority on Chatwoot's affiliate terms. Linking it beats asserting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stopped splitting my own signal.&lt;/strong&gt; I had four different affiliate codes across my site, one per content category, because I wanted to know which content converted. That experiment finished — one code produced everything, the other three produced nothing in three weeks — so I collapsed them into one. Four competing answers on one domain is a weaker signal than one answer repeated, and I'd been doing it to myself.&lt;/p&gt;

&lt;p&gt;I don't know yet whether any of it moves the needle. That's the honest ending.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you actually want Chatwoot
&lt;/h2&gt;

&lt;p&gt;Since it would be silly to write all this and be coy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting is free.&lt;/strong&gt; The Community Edition has no agent cap and no conversation cap. If you're comfortable with Docker, you do not need a discount code, because there's nothing to discount. This is genuinely what I'd recommend to most people reading dev.to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hosted version&lt;/strong&gt; has a free tier for up to 2 agents, then starts at $19/agent/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you go hosted&lt;/strong&gt;, the working code is &lt;code&gt;ACHIYAVS&lt;/code&gt; for 5% off at &lt;a href="https://www.chatwoot.com/?via=achiya-automation" rel="noopener noreferrer"&gt;Chatwoot Cloud&lt;/a&gt;, and yes, I earn a commission on it. On the $19 tier that discount is about $0.95 per agent per month. It is not a reason to choose anything. Take the 15-day trial first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd rather tell you the code is worth a dollar than tell you it's worth 70% and waste your time at checkout.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want to reproduce the measurement: search your own brand or code in quotes with &lt;code&gt;&amp;amp;nfpr=1&lt;/code&gt; appended to the Google URL to disable autocorrect, logged out. The number of distinct domains that come back is the number the models are effectively counting.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My fix for a data-loss bug sat in a green PR for four days. Every install in that window still had the bug.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Tue, 21 Jul 2026 11:12:56 +0000</pubDate>
      <link>https://dev.to/achiya-automation/my-fix-for-a-data-loss-bug-sat-in-a-green-pr-for-four-days-every-install-in-that-window-still-had-2jj5</link>
      <guid>https://dev.to/achiya-automation/my-fix-for-a-data-loss-bug-sat-in-a-green-pr-for-four-days-every-install-in-that-window-still-had-2jj5</guid>
      <description>&lt;p&gt;On July 17 I opened the most serious issue my project has ever had, wrote the fix the same morning, and pushed it as a pull request. CI went green on Node 20, 22 and 24 within minutes. Zero review comments. &lt;code&gt;mergeStateStatus: CLEAN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then it sat there.&lt;/p&gt;

&lt;p&gt;I merged it today — July 21. Four days.&lt;/p&gt;

&lt;p&gt;For those four days the fix existed, was correct, was tested, and was &lt;em&gt;visible to nobody&lt;/em&gt;. Every &lt;code&gt;npx safari-mcp&lt;/code&gt; in that window installed the version with the bug. The npm counter says roughly 1,400 people a week pull this thing. The fix was done and the users still had the bug, and those two facts sat side by side without touching each other.&lt;/p&gt;

&lt;p&gt;I want to be precise about why, because the interesting part isn't "I was busy."&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug, briefly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;safari-mcp&lt;/a&gt; lets an AI agent drive the Safari you're already logged into. That's the value and that's the danger: you're using the browser at the same time as the agent. The one promise the project makes is &lt;em&gt;the agent never touches a tab it didn't open.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That promise had a hole. When the tracked tab index ran past the end of the window — you closed a tab, or tore one into its own window — the resolver clamped the index to the last tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;tabCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[Safari MCP] Tab ghost proactive fix: clamping to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tabCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tabCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last tab in the window is not our tab. It's whatever &lt;em&gt;you&lt;/em&gt; happen to have open there. So at the exact moment the code discovers it no longer knows where its tab is, it points the session at one of yours — and logs the words "proactive fix" while doing it.&lt;/p&gt;

&lt;p&gt;The fix is four lines: fail closed. Drop ownership, return &lt;code&gt;null&lt;/code&gt;, let the next call open a fresh tab.&lt;/p&gt;

&lt;p&gt;Small diff. Green CI. Four days.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually held it
&lt;/h2&gt;

&lt;p&gt;Not review capacity. I'm the only reviewer; there was no queue.&lt;/p&gt;

&lt;p&gt;Not risk. The change makes an unsafe path safe; the worst case of shipping it is that the agent opens a redundant tab.&lt;/p&gt;

&lt;p&gt;What held it was that &lt;strong&gt;merging was the fourth step of a five-step ritual and only the first step was automated.&lt;/strong&gt; Merge, bump version, write the changelog entry, cut a GitHub release, and only then does the Publish workflow fire and npm gets the fix. Steps 2–4 are me, at a keyboard, in a mood to do release chores.&lt;/p&gt;

&lt;p&gt;CI told me the code was good. Nothing told me the code was &lt;em&gt;stuck&lt;/em&gt;. Those are different signals and I only had one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell I ignored
&lt;/h2&gt;

&lt;p&gt;Here's the part that made me write this instead of quietly shipping.&lt;/p&gt;

&lt;p&gt;While cutting today's release I opened &lt;code&gt;CHANGELOG.md&lt;/code&gt; and found the previous release, v2.15.3, had &lt;strong&gt;no entry at all&lt;/strong&gt;. Shipped July 15. The release commit touched exactly two files: &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three real fixes went out that day — clicks landing on the wrong tab, ARIA combobox typeahead never loading, a focus-helper queue that poisoned itself after a timeout, the last one contributed by an outside developer. Anyone reading the changelog to decide whether to upgrade saw nothing between 2.15.2 and 2.15.4. The contributor's fix was invisible.&lt;/p&gt;

&lt;p&gt;I didn't skip that entry on purpose. I skipped it because it's step 3 of the same manual ritual, and step 3 has no test.&lt;/p&gt;

&lt;p&gt;So the pattern isn't "a PR was slow." The pattern is: &lt;strong&gt;the parts of shipping that a machine watches are reliable, and the parts that only a human watches decay — silently, and in the same direction every time.&lt;/strong&gt; My CI has 57 tests. My release process has zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable comparison
&lt;/h2&gt;

&lt;p&gt;There's a well-worn instinct in solo open source that goes: &lt;em&gt;don't automate the release, you'll ship something bad by accident.&lt;/em&gt; Manual = careful.&lt;/p&gt;

&lt;p&gt;I believed that. Look at what manual actually bought me:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Automated&lt;/th&gt;
&lt;th&gt;Manual&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code correctness&lt;/td&gt;
&lt;td&gt;57 tests, 3 Node versions&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packaging&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;packaging.test.mjs&lt;/code&gt; fails CI if an imported file isn't published&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry sync&lt;/td&gt;
&lt;td&gt;server.json version synced + published on release&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merge decision&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;4-day gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Changelog entry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;silently missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cutting the release&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;mood-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every column with a machine in it held. Every column with only me in it drifted. The "careful" half is the half that failed, and it failed quietly, which is worse than failing loudly.&lt;/p&gt;

&lt;p&gt;The honest version of the instinct isn't &lt;em&gt;manual is careful.&lt;/em&gt; It's &lt;em&gt;manual feels careful because you were paying attention at the moment you did it, and you have no record of the moments you weren't.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm changing
&lt;/h2&gt;

&lt;p&gt;Not full auto-release-on-merge. I still want a human deciding when a release exists, because "these three fixes belong together" is a judgment call and I don't have a robot with taste.&lt;/p&gt;

&lt;p&gt;What I want is for the &lt;em&gt;gaps&lt;/em&gt; to be loud:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A changelog check in CI.&lt;/strong&gt; If the diff touches &lt;code&gt;safari.js&lt;/code&gt; or &lt;code&gt;index.js&lt;/code&gt; and doesn't touch &lt;code&gt;CHANGELOG.md&lt;/code&gt;, CI complains. That's the test step 3 never had. It's ten lines and it would have caught the 2.15.3 hole on the day it happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stale-green-PR alarm.&lt;/strong&gt; Any PR that is &lt;code&gt;CLEAN&lt;/code&gt; + passing + zero-review-comments for more than 48 hours gets surfaced, loudly, wherever I'll actually see it. Not to force a merge — to make "still holding this" a decision I re-make rather than a default I drift into.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release notes generated from the changelog&lt;/strong&gt;, so the GitHub release and the changelog can't disagree — one source, two renders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of that is clever. That's the point. The failure wasn't clever either.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I'd actually generalize
&lt;/h2&gt;

&lt;p&gt;If you maintain something alone, look at your last five releases and ask a narrow question: &lt;em&gt;which steps between "merged" and "a user can install it" have no automated observer?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not "which steps are manual" — plenty of manual steps are fine. Which manual steps would fail &lt;strong&gt;without producing a symptom you'd notice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A missing changelog entry produces no error. A green PR sitting for four days produces no error. A release you didn't cut produces no error. They produce the &lt;em&gt;absence&lt;/em&gt; of something, and absence doesn't page anybody.&lt;/p&gt;

&lt;p&gt;My whole test suite is built to catch code that does the wrong thing. Not one of those 57 tests can catch code that does the right thing where nobody can reach it.&lt;/p&gt;




&lt;p&gt;v2.15.4 is on npm now — the tab-index path fails closed, and the 2.15.3 changelog entry has been backfilled. Source: &lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;github.com/achiya-automation/safari-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the gap in your pipeline that has no observer?&lt;/strong&gt; I'm genuinely collecting these — the ones I find myself are always the boring ones, and the interesting ones seem to come from other people's setups.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>devops</category>
    </item>
    <item>
      <title>I filed a critical bug against my own tool. Then I read the code — and my own root cause was wrong.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Fri, 17 Jul 2026 07:19:32 +0000</pubDate>
      <link>https://dev.to/achiya-automation/i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root-cause-was-wrong-3692</link>
      <guid>https://dev.to/achiya-automation/i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root-cause-was-wrong-3692</guid>
      <description>&lt;p&gt;Three days ago I filed the most serious issue my project has ever had — against myself.&lt;/p&gt;

&lt;p&gt;The tool is &lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;safari-mcp&lt;/a&gt;, an MCP server that lets an AI agent drive the Safari you're already logged into. That premise is the whole value proposition, and it's also the whole danger: &lt;strong&gt;the user is using this browser at the same time as the agent.&lt;/strong&gt; The single promise the project makes is "the agent never touches a tab it didn't open."&lt;/p&gt;

&lt;p&gt;The issue was that the promise had broken. Two of a user's tabs ended up displaying pages the agent had loaded. Nothing was closed — the back-history survived — but scroll position, in-page state, anything unsaved: gone.&lt;/p&gt;

&lt;p&gt;I wrote up the incident, traced the root cause, and proposed a fix. Then today I opened the file to actually write that fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My root cause was wrong.&lt;/strong&gt; The fix I proposed had already shipped, three months ago. And the real bug was sitting four lines below it, wearing the word "fix" in its own log message.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I claimed
&lt;/h2&gt;

&lt;p&gt;Here's what I wrote in the issue:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;_ownedTabs&lt;/code&gt; (and every &lt;code&gt;tabIndex&lt;/code&gt; parameter) is a positional handle to a mutable, user-controlled list. Positional handles are only valid as long as nobody else mutates the list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the proposed fix:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Track ownership by a &lt;strong&gt;stable identity&lt;/strong&gt;, not a position. On &lt;code&gt;safari_new_tab&lt;/code&gt;, inject a sentinel into the page (e.g. &lt;code&gt;window.__safariMcpTabId = "&amp;lt;uuid&amp;gt;"&lt;/code&gt;), and resolve &lt;code&gt;tabIndex&lt;/code&gt; → real tab by scanning windows for the matching sentinel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Confident. Specific. Reasonable. I even left a follow-up comment calling the sentinel design "the plan of record."&lt;/p&gt;

&lt;p&gt;I wrote all of that from my memory of the architecture. I did not open &lt;code&gt;safari.js&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually in the file
&lt;/h2&gt;

&lt;p&gt;The sentinel already existed. It had been there since v2.8.3, released April 14 — a release literally titled &lt;em&gt;"bulletproof tab tracking via &lt;code&gt;window.__mcpTabMarker&lt;/code&gt;."&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ========== TAB IDENTITY MARKER ==========&lt;/span&gt;
&lt;span class="c1"&gt;//  - window.name           : survives EVERY navigation (full loads, redirects,&lt;/span&gt;
&lt;span class="c1"&gt;//                            cross-origin). The browser preserves window.name by&lt;/span&gt;
&lt;span class="c1"&gt;//                            design — the bulletproof identity that index/URL lack.&lt;/span&gt;
&lt;span class="c1"&gt;//  - window.__mcpTabMarker : survives SPA / same-document routing (secondary marker).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had written the fix I was now proposing. I'd written the &lt;em&gt;comment explaining why it was the right fix&lt;/em&gt;. I'd forgotten I'd done it.&lt;/p&gt;

&lt;p&gt;So the interesting question stopped being "why is ownership positional" — it isn't — and became &lt;strong&gt;"if identity resolution is already there, how did a user's tab still get navigated?"&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three exits, one of them guesses
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resolveActiveTab()&lt;/code&gt; is the function that answers "which tab is ours, right now?" It has a strategy ladder, and what matters is how each rung &lt;em&gt;fails&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 1 — the marker scan.&lt;/strong&gt; Loop every tab, ask each one whether &lt;code&gt;window.name&lt;/code&gt; matches our marker. Found it? That's our tab, whatever index it's sitting at. This is identity, and it's correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 2 — no URL to fall back on:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;hasOwnedTab&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabMarker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identity lost → drop the index. &lt;strong&gt;Fails closed.&lt;/strong&gt; ✅&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 3 — the URL scan comes back empty:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;hasOwnedTab&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabMarker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Safari MCP] Tab identity lost (marker + URL unresolved) — clearing index to avoid targeting the user&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;s tab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same call, same instinct. &lt;strong&gt;Fails closed.&lt;/strong&gt; ✅&lt;/p&gt;

&lt;p&gt;And then, four lines later, in the same block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;tabCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[Safari MCP] Tab ghost proactive fix: index &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt; tabCount &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tabCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, clamping to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tabCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tabCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that carefully, because I didn't for three months.&lt;/p&gt;

&lt;p&gt;We tracked tab 8. The window now has 7 tabs — because the user closed one, or tore one into its own window. Our index is out of range. That is the &lt;em&gt;exact&lt;/em&gt; moment the code has learned it no longer knows where our tab is.&lt;/p&gt;

&lt;p&gt;And it responds by &lt;strong&gt;clamping the index to &lt;code&gt;tabCount&lt;/code&gt;&lt;/strong&gt;: tab 7. The last tab in the window. A tab we have never seen, that belongs to the user, chosen for no reason other than that it's the highest index that won't throw.&lt;/p&gt;

&lt;p&gt;Then it logs the words &lt;strong&gt;"proactive fix."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the bug. Not positional ownership — a fail-open sitting between two fail-closed branches, in the safety-critical path, describing itself as a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it got there
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git log -S&lt;/code&gt; puts the clamp at March 31. The identity marker landed April 14.&lt;/p&gt;

&lt;p&gt;The clamp is &lt;strong&gt;two weeks older than the mechanism that made it obsolete.&lt;/strong&gt; It's from the era when the index was genuinely all we had, when "out of range" produced ugly AppleScript errors and clamping made them stop.&lt;/p&gt;

&lt;p&gt;And that's the actual lesson, the one that generalizes past my weird little macOS project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you add a better mechanism, the old heuristic does not remove itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The v2.8.3 work added identity resolution and correctly rewired the branches it was looking at — the two &lt;code&gt;hasOwnedTab &amp;amp;&amp;amp; !activeTabMarker&lt;/code&gt; guards &lt;em&gt;are&lt;/em&gt; the new thinking, and they fail closed because in April I understood the stakes. The clamp wasn't rewired, because it didn't look like an ownership decision. It looked like input validation. It looked like the &lt;em&gt;careful&lt;/em&gt; line. It had a bounds check and an error log.&lt;/p&gt;

&lt;p&gt;Every upgrade leaves fossils like this. The dangerous ones aren't the code that looks scary. They're the code that looks like it's on your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Proactive fix" is a confession
&lt;/h2&gt;

&lt;p&gt;Here's the tell I want to hand you, because it's cheap and it's reusable.&lt;/p&gt;

&lt;p&gt;The clamp was written to fix a &lt;em&gt;symptom&lt;/em&gt;: an index pointing past the end of the array. It makes that symptom disappear by &lt;strong&gt;inventing a plausible value&lt;/strong&gt;. &lt;code&gt;tabCount&lt;/code&gt; isn't a computed answer to "where is our tab" — it's the nearest number that doesn't crash.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any code that converts "I don't know" into a plausible value is a fail-open wearing a fix's clothes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you have that phrasing, you start seeing them everywhere. &lt;code&gt;?? 0&lt;/code&gt; on a total that should have been fetched. &lt;code&gt;catch {}&lt;/code&gt; around the call that establishes permission. &lt;code&gt;|| user[0]&lt;/code&gt; when the lookup missed. Clamping an index into range. Each one takes a state where the honest answer is &lt;em&gt;stop&lt;/em&gt; and launders it into a value the next line will happily use.&lt;/p&gt;

&lt;p&gt;The bounds check is real. The clamp is the bug. They're on the same line, and that's exactly why it survived three months of me reading past it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the fix actually is
&lt;/h2&gt;

&lt;p&gt;It's a deletion, not the sentinel architecture I proposed. That branch has to do what its two neighbours already do — when the index is out of range, identity is lost, so drop it and make the caller re-anchor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;tabCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;_st&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;activeTabIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// fail closed, like every other exit&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Callers already handle this. There's a &lt;code&gt;_assertNotFallingBackToUserTab()&lt;/code&gt; that throws a clear &lt;em&gt;"Tab tracking lost — re-run &lt;code&gt;safari_new_tab&lt;/code&gt; to recover"&lt;/em&gt;. The recovery path was built. The clamp was just routing around it.&lt;/p&gt;

&lt;p&gt;The cost is honest: sessions that used to get lucky will now throw. In a tool whose entire promise is &lt;em&gt;"we don't touch your tabs,"&lt;/em&gt; an error message is the correct output for "I don't know which tab is mine." A guess is not.&lt;/p&gt;

&lt;p&gt;It's &lt;a href="https://github.com/achiya-automation/safari-mcp/pull/59" rel="noopener noreferrer"&gt;PR #59&lt;/a&gt;, open against &lt;a href="https://github.com/achiya-automation/safari-mcp/issues/54" rel="noopener noreferrer"&gt;issue #54&lt;/a&gt; — where I've also left my original root-cause analysis standing, wrong, with a correction under it. The drift between what I remembered and what shipped &lt;em&gt;is&lt;/em&gt; the bug; editing the evidence out seemed like the wrong move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I keep thinking about
&lt;/h2&gt;

&lt;p&gt;I filed a detailed, confident, well-structured bug report about code I wrote, and got the root cause wrong — because I reasoned from my mental model instead of from the file. The mental model was a year of accumulated intent. The file was what actually shipped. Those had quietly drifted apart, and the gap between them is precisely where the bug lived.&lt;/p&gt;

&lt;p&gt;If I'd handed that issue to an AI agent — or a new contributor — they'd have implemented the sentinel I asked for. Diligently. It already existed. The clamp would still be there, and the user's tabs would still be getting navigated, and the issue would be closed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The most expensive thing in that whole chain was my confidence.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Have you found one of these in your own code — a "fix" that was actually inventing an answer? I'd genuinely like to collect the pattern. Drop it in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;safari-mcp&lt;/a&gt; is MIT-licensed and drives your real, logged-in Safari on macOS. It has 97 tools, and — once #59 lands — one fewer place where it guesses which tab is yours.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>A stranger fixed my bug. Then I found out he fixed the wrong half — and it still worked.</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Sun, 12 Jul 2026 07:16:34 +0000</pubDate>
      <link>https://dev.to/achiya-automation/a-stranger-fixed-my-bug-then-i-found-out-he-fixed-the-wrong-half-and-it-still-worked-309g</link>
      <guid>https://dev.to/achiya-automation/a-stranger-fixed-my-bug-then-i-found-out-he-fixed-the-wrong-half-and-it-still-worked-309g</guid>
      <description>&lt;p&gt;Someone opened a pull request against my Safari MCP server last week. Three functions, same treatment each, clean write-up, all CI green. It fixed a real bug — his repro was solid, mine reproduced it too.&lt;/p&gt;

&lt;p&gt;Then I sat down to write it up, built a 40-line model of the thing to make sure I understood it, and discovered that &lt;strong&gt;half of his patch does nothing at all.&lt;/strong&gt; Not "does something subtle." Nothing. The two hunks he described as the fix are behaviorally identical to the code they replace.&lt;/p&gt;

&lt;p&gt;The half he &lt;em&gt;didn't&lt;/em&gt; emphasize is the one that closes the bug. And the reason neither of us could tell — the reason I couldn't tell about my own code — turns out to be the actual story here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol: positional, ID-free, and one mistake from disaster
&lt;/h2&gt;

&lt;p&gt;My server talks to a small Swift helper for the things JavaScript can't do on a Mac: focus an app, hide a window, synthesize a real OS-level click. Newline-delimited JSON over stdin/stdout. Request and response are correlated &lt;strong&gt;positionally&lt;/strong&gt; — there are no request IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// callbacks waiting for responses&lt;/span&gt;

&lt;span class="nx"&gt;_helperProc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;_buf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;_buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// first reply belongs to first waiter&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works exactly as long as one invariant holds:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every callback in the queue corresponds to a request that was actually sent and whose reply is still coming.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Violate that once and the queue is off by one. Reply N goes to waiter N+1. Forever — there is nothing in the protocol that can ever resynchronize it. And because every waiter is a &lt;code&gt;setTimeout&lt;/code&gt;-guarded promise, the symptom is never an error. It's a hang.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two ways to abandon a request
&lt;/h2&gt;

&lt;p&gt;A helper call looks like this (this is the real pre-fix code, lightly trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;_helperGetFrontApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_withHelperLock&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;        &lt;span class="c1"&gt;// path 1: gave up waiting&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                                       &lt;span class="c1"&gt;// ← pushed BEFORE the write&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_helperProc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"getFrontApp":true}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                &lt;span class="c1"&gt;// path 2: write blew up&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two ways to walk away from a request, and they are &lt;strong&gt;not&lt;/strong&gt; the same, which is the whole point:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 1 — the timeout.&lt;/strong&gt; The helper is slow; we stop waiting. But a reply is still coming. The dead &lt;code&gt;cb&lt;/code&gt; stays in the queue, gets shifted when the late line lands, sees &lt;code&gt;resolved === true&lt;/code&gt;, and returns without touching anything. It ate exactly one line — which is precisely correct, because exactly one line was owed. &lt;strong&gt;The queue stays aligned.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 2 — the write failure.&lt;/strong&gt; &lt;code&gt;stdin.write&lt;/code&gt; throws (broken pipe: the helper died between our last call and this one). The request was &lt;strong&gt;never sent&lt;/strong&gt;, so no reply is ever coming. But look at the &lt;code&gt;catch&lt;/code&gt;: it calls &lt;code&gt;resolve(null)&lt;/code&gt; and &lt;strong&gt;never sets &lt;code&gt;resolved = true&lt;/code&gt;.&lt;/strong&gt; So &lt;code&gt;cb&lt;/code&gt; is still sitting in the queue, and it's still &lt;em&gt;armed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The next request comes in. Its reply arrives. &lt;code&gt;shift()&lt;/code&gt; hands it to the armed corpse of a request that was never sent. &lt;code&gt;resolved&lt;/code&gt; is false, so it consumes the line, resolves a promise nobody is listening to, and &lt;strong&gt;the caller who actually sent a request gets nothing&lt;/strong&gt; — it waits out its full timeout, and leaves its own callback behind when it does.&lt;/p&gt;

&lt;p&gt;That's not an off-by-one. That's a cascade. One failed write poisons the channel for the lifetime of the daemon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it instead of believing it
&lt;/h2&gt;

&lt;p&gt;I've been wrong about my own concurrency code before, so I stopped arguing with myself and modeled it — a positional queue, the same &lt;code&gt;resolved&lt;/code&gt; guard, the two abandonment paths, pre-fix and post-fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PRE-FIX   timeout   → B got: reply-to-B     ← already correct!
PRE-FIX   writefail → B got: TIMEOUT(B)     ← B's reply eaten by A's armed callback
POST-FIX  timeout   → B got: reply-to-B
POST-FIX  writefail → B got: reply-to-B     ← fixed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it is. The timeout path was &lt;strong&gt;never broken.&lt;/strong&gt; The patch's headline change — replacing the abandoned callback with an explicit no-op consumer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;   &lt;span class="c1"&gt;// no-op consumer for the late reply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...replaces a callback that was &lt;em&gt;already&lt;/em&gt; a no-op consumer (because &lt;code&gt;cb&lt;/code&gt; self-guards on &lt;code&gt;resolved&lt;/code&gt;) with a callback that is &lt;em&gt;visibly&lt;/em&gt; a no-op consumer. Zero behavior change. It's a comment that happens to compile.&lt;/p&gt;

&lt;p&gt;The fix is the other hunk, the quiet one in the &lt;code&gt;catch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// never sent → nothing is coming → drop the slot&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And note that the two hunks do &lt;strong&gt;opposite&lt;/strong&gt; things — one &lt;em&gt;keeps&lt;/em&gt; the slot, one &lt;em&gt;removes&lt;/em&gt; it — for reasons that are entirely non-obvious unless you have the positional invariant in your head. Swap them and you've built a worse bug than the one you set out to fix: &lt;code&gt;splice&lt;/code&gt; on timeout means the late reply gets shifted onto the next caller, who now gets someone else's answer and believes it. He got that distinction right in both directions, on his first try, in code he didn't write. That's the part I'm actually impressed by.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reason nobody could see it
&lt;/h2&gt;

&lt;p&gt;Here's what I found when I grepped my own file for this pattern. Seven helper functions. Every one of them open-codes the same request/timeout/queue dance. Four carry some version of the no-op-consumer line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;764&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_helperConsecutiveTimeouts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// late reply ⇒ alive&lt;/span&gt;
&lt;span class="mi"&gt;825&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;   &lt;span class="c1"&gt;// no-op consumer for a late reply&lt;/span&gt;
&lt;span class="mi"&gt;863&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;   &lt;span class="c1"&gt;// No-op consumer for late response&lt;/span&gt;
&lt;span class="mi"&gt;912&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;_helperQueue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three don't. Those three are exactly the three he patched.&lt;/p&gt;

&lt;p&gt;So the honest reading isn't "I forgot the fix in three places." It's worse and more interesting: &lt;strong&gt;I wrote a defensive line four times without ever writing down what it defends against&lt;/strong&gt;, and in the three functions where I skipped it, the code was &lt;em&gt;accidentally fine anyway&lt;/em&gt; — while a genuinely broken path sat two lines below, in every single one of the seven.&lt;/p&gt;

&lt;p&gt;The invariant — &lt;em&gt;an abandoned slot must consume exactly one reply, unless nothing was ever sent&lt;/em&gt; — is stated in exactly zero of those seven functions. Not in a comment, not in a name, not in a type. It lives only in whatever I happened to be holding in my head on the afternoon I wrote each one. A contributor reading this file has no way to check his patch against the rule, because the rule isn't there. He had to reconstruct it from the wreckage — and he reconstructed it correctly, which is why his &lt;code&gt;catch&lt;/code&gt; hunk lands even though his description of &lt;em&gt;why&lt;/em&gt; doesn't.&lt;/p&gt;

&lt;p&gt;That's the failure. Not the missing line. The missing &lt;strong&gt;place to put the line.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy-paste isn't a style problem, it's a memory problem
&lt;/h2&gt;

&lt;p&gt;I don't care much about DRY as an aesthetic. A little duplication beats a bad abstraction, and I'll take three copies of a five-line function over an inheritance hierarchy every day of the week.&lt;/p&gt;

&lt;p&gt;But that argument is about &lt;em&gt;code you can see&lt;/em&gt;. This is about something else:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A rule that exists in N copies has to be &lt;strong&gt;re-derived from scratch&lt;/strong&gt; by whoever writes copy N+1 — including you, next year, at 11pm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Seven functions, seven independent recollections of an unwritten rule. I got the cosmetic half right four times out of seven and the load-bearing half wrong seven times out of seven. That is not a discipline problem you fix by being more careful. It's a coin flip you have to win every time, forever, and the odds get worse with every function added.&lt;/p&gt;

&lt;p&gt;The correct shape was always one function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The helper protocol is POSITIONAL — replies are matched to requests by queue order,&lt;/span&gt;
&lt;span class="c1"&gt;// not by id. So an abandoned request must leave the queue in a consistent state:&lt;/span&gt;
&lt;span class="c1"&gt;//   • timed out  → a reply IS still coming → leave a no-op consumer to eat exactly one line&lt;/span&gt;
&lt;span class="c1"&gt;//   • write failed → nothing was ever sent → remove the slot entirely&lt;/span&gt;
&lt;span class="c1"&gt;// Get this backwards and every subsequent reply goes to the wrong caller, silently, forever.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;_helperRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onAbandon&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven callers, one invariant, stated once, in the only place where it can be found by the person who needs it. Copy eight — written by someone who never read this article — gets it for free.&lt;/p&gt;

&lt;p&gt;That refactor is mine to do, and it's the actual fix. His patch fixed the three instances. The reason there were three instances to fix is still sitting in my file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this says about test suites
&lt;/h2&gt;

&lt;p&gt;54 tests on this file. All green. Not one of them ever failed a write.&lt;/p&gt;

&lt;p&gt;Of course they didn't — you have to go out of your way to break a pipe in a test, and nobody writes that test until the bug that needs it has already shipped. The bug lived in the intersection of two things a test suite naturally avoids: the error path of the transport, and &lt;em&gt;state that outlives a single call&lt;/em&gt;. My tests all assert what one request returns. The bug is only visible in what the &lt;strong&gt;next&lt;/strong&gt; one returns.&lt;/p&gt;

&lt;p&gt;He didn't find it by reading my code looking for bugs. He found it because &lt;code&gt;safari_new_tab&lt;/code&gt; hung on his machine, and he kept going past the symptom until he hit a queue that was one slot off.&lt;/p&gt;




&lt;p&gt;Do you have a positional protocol in production — FIFO correlation, no request IDs? I want to know whether "an abandoned slot must eat exactly one reply" is folklore that everyone rediscovers the hard way, or whether the real lesson is just &lt;em&gt;don't build a protocol without request IDs&lt;/em&gt;. Because I'm now fairly sure the invariant I'm about to carefully centralize is one I shouldn't need to have at all.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://github.com/achiya-automation/safari-mcp" rel="noopener noreferrer"&gt;achiya-automation/safari-mcp&lt;/a&gt; — a Safari MCP server that drives your real, logged-in browser. The PR is &lt;a href="https://github.com/achiya-automation/safari-mcp/pull/53" rel="noopener noreferrer"&gt;#53&lt;/a&gt;, by &lt;a href="https://github.com/jrepp" rel="noopener noreferrer"&gt;@jrepp&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>debugging</category>
      <category>opensource</category>
    </item>
    <item>
      <title>One command adds import, WhatsApp drip and campaigns to self-hosted Chatwoot</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Sun, 05 Jul 2026 13:12:46 +0000</pubDate>
      <link>https://dev.to/achiya-automation/one-command-adds-import-whatsapp-drip-and-campaigns-to-self-hosted-chatwoot-mkh</link>
      <guid>https://dev.to/achiya-automation/one-command-adds-import-whatsapp-drip-and-campaigns-to-self-hosted-chatwoot-mkh</guid>
      <description>&lt;p&gt;I run a self-hosted &lt;a href="https://www.chatwoot.com/" rel="noopener noreferrer"&gt;Chatwoot&lt;/a&gt; for support, and I genuinely love it. But every time I wanted to actually &lt;em&gt;grow&lt;/em&gt; on it, I hit the same wall.&lt;/p&gt;

&lt;p&gt;Import a few thousand contacts? Hand-write API calls, or click them in one at a time. Run WhatsApp follow-up sequences? Not built in. Send a bulk campaign with a real variable preview, or attach a video over WhatsApp's 16 MB limit? Nope.&lt;/p&gt;

&lt;p&gt;And every off-the-shelf "fix" was the same shape: a separate SaaS, a second server, or a subdomain — each with its own login and its own copy of my customers' data.&lt;/p&gt;

&lt;p&gt;So I built the missing layer and open-sourced it: &lt;strong&gt;&lt;a href="https://github.com/achiya-automation/chatwoot-power-tools" rel="noopener noreferrer"&gt;chatwoot-power-tools&lt;/a&gt;&lt;/strong&gt; (MIT).&lt;/p&gt;

&lt;h2&gt;
  
  
  One command, same-origin, no second server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://github.com/achiya-automation/chatwoot-power-tools/archive/refs/heads/main.tar.gz | &lt;span class="nb"&gt;tar &lt;/span&gt;xz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;chatwoot-power-tools-main &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer detects your existing Chatwoot Docker Compose stack, provisions a least-privilege DB role, starts one small sidecar container (&lt;code&gt;cwpt-engine&lt;/code&gt;) next to your own containers, adds a single reverse-proxy route, and injects a dashboard script. Everything it adds is served &lt;strong&gt;same-origin&lt;/strong&gt; under one &lt;code&gt;/chatwoot-addons/*&lt;/code&gt; path — no subdomain, no CORS, no extra account, and no customer data ever leaving your box.&lt;/p&gt;

&lt;p&gt;It's a plain, readable Bash installer (no opaque binary piped to root), it's &lt;code&gt;--dry-run&lt;/code&gt;-previewable, and &lt;code&gt;--uninstall&lt;/code&gt; reverses everything while preserving your data and any existing &lt;code&gt;DASHBOARD_SCRIPTS&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it adds
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;📥 Smart contact import&lt;/strong&gt; — a CSV/Excel wizard styled to look native, detects columns bilingually (Hebrew + English headers), flags duplicates &lt;em&gt;before&lt;/em&gt; import, and maps onto custom attributes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔁 WhatsApp drip sequences&lt;/strong&gt; — automated template-message sequences managed from inside Chatwoot. Enroll a lead by setting a conversation attribute; messages then send at the intervals you configure, automatically skipping quiet hours, Shabbat and Jewish holidays.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✨ Dashboard upgrades&lt;/strong&gt; — a "Sequences" sidebar item, variable chips + a live preview on the native campaign modal, and client-side video compression (WebCodecs) so you can send video past WhatsApp's 16 MB limit with no server-side transcode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm most proud of: least-privilege by design
&lt;/h2&gt;

&lt;p&gt;The engine talks to your Chatwoot only through the API, and its database role is deliberately tiny. It gets &lt;code&gt;SELECT&lt;/code&gt; on the handful of tables it reads, plus &lt;code&gt;UPDATE&lt;/code&gt; on a &lt;strong&gt;single column&lt;/strong&gt; — &lt;code&gt;contacts.custom_attributes&lt;/code&gt;. That's it.&lt;/p&gt;

&lt;p&gt;It literally &lt;em&gt;cannot&lt;/em&gt; read or change names, phones, emails, or anything else. A bug in the engine can't touch them, because the grant doesn't exist. The role's password is generated on your server with &lt;code&gt;openssl rand&lt;/code&gt; and written only to your Chatwoot &lt;code&gt;.env&lt;/code&gt; — it never enters logs, command output, or git. No telemetry, no third parties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fully bilingual, automatically
&lt;/h2&gt;

&lt;p&gt;The entire UI localizes to each agent's own Chatwoot language — Hebrew (RTL) or English (LTR) — detected automatically, no configuration. Same screen, either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It's MIT, self-hosted Docker Compose only (not Chatwoot Cloud), with a full CI suite. If you self-host Chatwoot, I'd genuinely love your feedback — and issues/PRs are welcome.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/achiya-automation/chatwoot-power-tools" rel="noopener noreferrer"&gt;github.com/achiya-automation/chatwoot-power-tools&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Still deciding between self-hosted and Cloud?
&lt;/h2&gt;

&lt;p&gt;This tool only makes sense if you're self-hosting, so it's worth saying plainly where that stops being the right call. Self-hosting is free and unlimited, but somebody has to own the server, the upgrades and the backups. If that somebody doesn't exist on your team, Chatwoot Cloud starts at $19/agent/month and the Hacker tier is free for up to 2 agents — I wrote up &lt;a href="https://achiya-automation.com/en/blog/chatwoot-vs-intercom/" rel="noopener noreferrer"&gt;the full pricing and where Intercom is genuinely the better buy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One warning if you go that route: search for a Chatwoot coupon and you'll find codes advertised at 20%, 50%, even 70% off. None of them work. Chatwoot's affiliate programme caps the customer discount at 5% — that's the ceiling, published on &lt;a href="https://www.chatwoot.com/affiliate-program" rel="noopener noreferrer"&gt;their own affiliate page&lt;/a&gt;. My code is &lt;code&gt;ACHIYAVS&lt;/code&gt; and it gives exactly that 5%, on monthly or yearly, on every paid tier.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: &lt;code&gt;ACHIYAVS&lt;/code&gt; is my affiliate code — I earn a commission if you use it, and you pay 5% less than list. Self-hosting with the tool above earns me nothing, and it's still what I'd recommend if you have the ops capacity.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>docker</category>
      <category>whatsapp</category>
    </item>
    <item>
      <title>Exactly-Once by Default: How Durable Execution Changed the Way I Build Automations</title>
      <dc:creator>אחיה כהן</dc:creator>
      <pubDate>Thu, 02 Jul 2026 11:21:39 +0000</pubDate>
      <link>https://dev.to/achiya-automation/exactly-once-by-default-how-durable-execution-changed-the-way-i-build-automations-2gbm</link>
      <guid>https://dev.to/achiya-automation/exactly-once-by-default-how-durable-execution-changed-the-way-i-build-automations-2gbm</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/achiya-automation/i-deleted-my-no-code-automation-platform-and-rewrote-34-workflows-in-typescript-emh"&gt;previous article&lt;/a&gt; I described moving 34 production automations off a visual no-code platform and rewriting them in TypeScript. The single feature that made that migration worth the effort was &lt;strong&gt;durable execution with exactly-once semantics&lt;/strong&gt;. This post is the deep-dive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: a crash in the middle
&lt;/h2&gt;

&lt;p&gt;Here's a scenario every automation eventually hits. A workflow receives a new lead, sends them a welcome message, then writes them to the CRM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send welcome message&lt;/li&gt;
&lt;li&gt;Save to CRM&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now imagine the process crashes &lt;em&gt;exactly&lt;/em&gt; between step 1 and step 2 — a deploy, an OOM kill, a dropped node. What happens on restart?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Re-run the whole thing&lt;/strong&gt; → the lead gets the welcome message twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't re-run it&lt;/strong&gt; → the lead never lands in the CRM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both outcomes are wrong. This is the at-least-once vs at-most-once dilemma, and in a system doing real side effects (sending messages, charging cards, creating records) it is not academic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual fix, and why it hurts
&lt;/h2&gt;

&lt;p&gt;Most tools give you retry-on-failure. But retry alone re-runs side effects. To get &lt;em&gt;exactly-once&lt;/em&gt; you build it yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate an idempotency key per lead.&lt;/li&gt;
&lt;li&gt;Before each side effect, check "did I already do this?" against some store.&lt;/li&gt;
&lt;li&gt;Persist progress after each step so a restart knows where to resume.&lt;/li&gt;
&lt;li&gt;Repeat this bookkeeping for every workflow you ever write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works, but it's tedious, easy to get subtly wrong, and it clutters every automation with plumbing that has nothing to do with the business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How DBOS makes it the default
&lt;/h2&gt;

&lt;p&gt;DBOS flips this: durability is the baseline, not a feature you assemble. You annotate ordinary TypeScript functions. A &lt;strong&gt;workflow&lt;/strong&gt; orchestrates; &lt;strong&gt;steps&lt;/strong&gt; are the units that do side effects and get checkpointed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DBOS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@dbos-inc/dbos-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Onboarding&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;DBOS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;welcomeLead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Onboarding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendWelcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// step 1&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Onboarding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveToCRM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// step 2&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;DBOS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sendWelcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;whatsapp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Welcome aboard!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;DBOS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;saveToCRM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the workflow runs, DBOS records the completion of each step in Postgres. From the docs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If a workflow is interrupted for any reason (e.g., an executor restarts or crashes), when your program restarts the workflow automatically resumes execution from the last completed step."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And crucially:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Steps are tried at least once but are never re-executed after they complete."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So in our crash scenario: &lt;code&gt;sendWelcome&lt;/code&gt; already completed and was recorded. On restart, DBOS &lt;strong&gt;skips it&lt;/strong&gt; and resumes at &lt;code&gt;saveToCRM&lt;/code&gt;. The welcome message is not sent twice; the CRM write finally happens. Exactly-once, with zero idempotency bookkeeping in my code.&lt;/p&gt;

&lt;p&gt;No separate workflow server, no queue broker to babysit — just your program and Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule to internalize
&lt;/h2&gt;

&lt;p&gt;Durability isn't free magic — there's a contract. The &lt;strong&gt;workflow function must be deterministic&lt;/strong&gt;: given the same recorded step results, replaying it must take the same path. So anything non-deterministic — network calls, random values, reading the clock, DB writes — belongs &lt;strong&gt;inside a step&lt;/strong&gt;, never loose in the workflow body. Steps are the checkpointed boundary; the workflow is the recomposable script that ties them together.&lt;/p&gt;

&lt;p&gt;Once that clicks, the mental model is clean: &lt;em&gt;workflow = the plan, steps = the effects&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this replaced
&lt;/h2&gt;

&lt;p&gt;On the visual platform, I got retry and error branches, but exactly-once across a crash was something I had to design per flow — manual idempotency keys and "already done?" checks. Here it's the substrate. My code shrank to the business logic, and the reliability guarantee got &lt;em&gt;stronger&lt;/em&gt;, not weaker.&lt;/p&gt;

&lt;p&gt;That reliability is also what I sell to clients: fewer leads slipping through the cracks, no duplicate messages, no half-finished processes. (See the client-facing angle in the LinkedIn series.)&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on how I built it
&lt;/h2&gt;

&lt;p&gt;I'm one person, and wiring durable execution into 34 real automations is a lot of surface area. I did it in pairing with &lt;strong&gt;Claude Code&lt;/strong&gt; — it turned "I understand exactly-once in theory" into workflows running in production, TypeScript module by TypeScript module. The barrier between a concept and a shipped system is thinner than it's ever been.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://docs.dbos.dev/typescript/tutorials/workflow-tutorial" rel="noopener noreferrer"&gt;DBOS Workflows tutorial&lt;/a&gt; · &lt;a href="https://docs.dbos.dev/typescript/reference/workflows-steps" rel="noopener noreferrer"&gt;Workflows &amp;amp; Steps reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How do you handle mid-workflow crashes today — hand-rolled idempotency, an outbox, something else? Curious what patterns people have settled on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>backend</category>
      <category>tutorial</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
