<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://fromparth.blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fromparth.blog/" rel="alternate" type="text/html" /><updated>2026-07-22T16:10:29+00:00</updated><id>https://fromparth.blog/feed.xml</id><title type="html">Parth Batra | fromparth.blog</title><subtitle>Senior ML Engineer by day, music critic by night. Building ML systems, writing about tech, and collecting spotify minutes like they&apos;re infinity stones.</subtitle><author><name>Parth Batra</name></author><entry><title type="html">For 30 Years, Bending Your Editor to Fit You Was a Vim Thing. Now It’s the Whole Job.</title><link href="https://fromparth.blog/2026/07/22/agent-quickpick.html" rel="alternate" type="text/html" title="For 30 Years, Bending Your Editor to Fit You Was a Vim Thing. Now It’s the Whole Job." /><published>2026-07-22T00:00:00+00:00</published><updated>2026-07-22T00:00:00+00:00</updated><id>https://fromparth.blog/2026/07/22/agent-quickpick</id><content type="html" xml:base="https://fromparth.blog/2026/07/22/agent-quickpick.html"><![CDATA[<p><a href="https://github.com/dataguyofprocol/nvim-config">My Neovim config</a> is 753 lines of Lua across 20 files and thirty-eight plugins. A function picks one of five colorschemes at random when I open the editor, because I couldn’t decide. There’s a keybinding that runs Conway’s Game of Life on my buffer, and a plugin I wrote and published, <a href="/2025/12/07/i-shipped-a-neovim-plugin-in-lua.html">hexwitch</a>, that turns a plain-English description into a whole colorscheme. None of it shipped with Vim. All of it is load-bearing. Nobody else could sit down at my machine and find anything.</p>

<p>For thirty years, doing this to your editor was a Vim thing. Practically a personality disorder with a plugin manager. You bent the tool to fit you, and everyone else adopted whatever came in the box. That split, the tinkerers and the adopters, was stable for my entire career.</p>

<p>It just ended. Not because more people found Vim. Because building the exact tool you want stopped being expensive.</p>

<p>There’s a name for the tinkerer side, and it isn’t mine. TJ DeVries, who works on Neovim itself, calls it a PDE. Personalized Development Environment. He coined it to make a point: Vim was never an IDE, it’s a thing you bend until it fits one person, you, and only you. The term was built to describe the <em>opposite</em> of VS Code. Which is the whole joke, because VS Code is where this is happening now.</p>

<p>Three out of four developers use VS Code. 75.9% in last year’s Stack Overflow survey, ninth year running at number one, and that undercounts it, because Cursor, Windsurf, and the whole wave of AI editors people are switching to are VS Code underneath, forks of the same open-source core. A hundred thousand extensions in the marketplace as of May. A hundred thousand ways people have already bent the default until it stops being anyone’s default. The tinkerer instinct TJ was describing in a niche editor now sits under three-quarters of the profession. Most of them just don’t know they’re allowed to use it.</p>

<p>I found out I was allowed at work, building agents.</p>

<p>The project I spend my days on runs eight code-review agents on the same diff at once. Seven I wrote, an eighth hands off to CodeRabbit. Architecture, security, concurrency, database performance, each one a specialist reading the change in parallel, an orchestrator merging what they find and dropping the low-confidence noise so I’m not drowning. Here’s the part worth staring at: the seven agents are seven markdown files in a folder. The hard part, the thing that sounds like a product, is text shaped like colleagues, and the CLI just reads it. Once you’ve seen that, you stop waiting for anyone to build it for you.</p>

<p>So the interesting question stopped being which tool to adopt. Look at what exists for running many agents at once: cmux, herdr, Conductor, the orchestration baked into Cursor and Windsurf. They’re good, and they’re all the same signal, the tools racing to hold your fleet of agents. I’m not betting against any of them. I’m saying I didn’t have to pick one.</p>

<p>I wanted my agents living in the editor, in tabs, next to the code, not in a separate window I check like a build server. So one afternoon I <a href="https://github.com/dataguyofprocol/agent-quickpick">built that</a>. A keybinding, a picker, each agent opens in its own tab with its own icon and its own color, Claude and Aider and Codex side by side, the ones I run most floating to the top. It runs on Cursor and Windsurf too, because they’re the same editor underneath. I didn’t file an issue. I didn’t wait for a roadmap. The distance from wanting it to having it was one evening.</p>

<p><img src="/assets/images/agent-quickpick-terminals.png" alt="Two agents open as side-by-side editor tabs, Claude on the left and OpenCode on the right, each with its own icon and label." /></p>

<p><img src="/assets/images/agent-quickpick-demo.gif" alt="Hitting the keybinding, picking an agent, and watching it open in its own tab." /></p>

<p>I should be honest about how far it goes, which is not very. Right now it launches agents and then forgets about them. The tab that would make it actually useful, one that shows every agent and what it’s doing so you can watch the fleet without leaving the window where you’d fix what it broke, I haven’t built yet. That’s next. So take the confidence with the appropriate discount.</p>

<hr />

<p>For most of software history, if you wanted your tools to work differently you had two moves: switch to something else someone built, or file a request and wait. Building it yourself was a real third option, and almost nobody took it, because it cost more than it was worth. That’s the part that changed. The cost fell through the floor, and it took the whole build-versus-adopt calculation down with it.</p>

<p>The story here isn’t which agent tool wins. It’s that the gap between wanting a tool and having it collapsed to an evening, and most of us are still standing in the line we’ve always stood in.</p>

<p>The word came from the editor that refused to be an IDE. It turned out it was never really about the editor. It was about who gets to decide how your tools work. For thirty years that was a small club. The door’s open now, and admission costs an afternoon.</p>

<p><em>TJ DeVries coined “PDE” for the editor that isn’t an IDE. The idea outgrew the editor.</em></p>]]></content><author><name>Parth Batra</name></author><category term="ai &amp; tech" /><summary type="html"><![CDATA[My Neovim config is 753 lines of Lua across 20 files and thirty-eight plugins. A function picks one of five colorschemes at random when I open the editor, because I couldn’t decide. There’s a keybinding that runs Conway’s Game of Life on my buffer, and a plugin I wrote and published, hexwitch, that turns a plain-English description into a whole colorscheme. None of it shipped with Vim. All of it is load-bearing. Nobody else could sit down at my machine and find anything.]]></summary></entry><entry><title type="html">Why 72 km/h Is the Perfect Speed for an Indian Highway. Not 70, Not 75, Not 80.</title><link href="https://fromparth.blog/2026/07/19/i-drive-at-exactly-72-kmh.html" rel="alternate" type="text/html" title="Why 72 km/h Is the Perfect Speed for an Indian Highway. Not 70, Not 75, Not 80." /><published>2026-07-19T00:00:00+00:00</published><updated>2026-07-19T00:00:00+00:00</updated><id>https://fromparth.blog/2026/07/19/i-drive-at-exactly-72-kmh</id><content type="html" xml:base="https://fromparth.blog/2026/07/19/i-drive-at-exactly-72-kmh.html"><![CDATA[<p>For the last couple of years, I did most of my long drives along NH48, the Delhi-Jaipur Highway, in a Suzuki Swift. Cruise control on, Rafi and Kishore Kumar on the speakers, because that’s the only music that survives four hours of highway without becoming annoying. Over months of nudging the cruise control dial a notch at a time, I kept landing on the same number. Not 70. Not 75. Just 72.</p>

<p>Until today, if you’d asked me why 72, my answer would have been that it feels right. That it sounds cool. Nothing I could defend with a straight face.</p>

<p>So I spent an entire evening doing the math to defend a “vibe”. The question: take the things that actually matter on a typical Indian highway (stopping in time, good fuel economy, safe distance from the vehicle ahead, traffic flow), work out the speed range each one allows, and see where they overlap. I expected a wide, boring band, maybe 60 to 90, with 72 sitting somewhere in the middle. But that isn’t what happened.</p>

<figure class="diagram">
<svg viewBox="0 0 380 215" width="100%" height="auto" role="img" aria-label="A speedometer dial with the marker sitting at 72 km/h, which is 20 metres per second." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
    <!-- rest of the dial, 72 to top -->
    <path d="M235.4 40.9 A100 100 0 0 1 260.7 200.7" stroke-width="3.4" opacity="0.2" />
    <!-- dial filled to 72 -->
    <path d="M119.3 200.7 A100 100 0 0 1 235.4 40.9" stroke-width="3.4" />
    <!-- ticks -->
    <g stroke-width="1.6" opacity="0.45">
      <line x1="119.3" y1="200.7" x2="127.8" y2="192.2" />
      <line x1="90" y1="130" x2="102" y2="130" />
      <line x1="119.3" y1="59.3" x2="127.8" y2="67.8" />
      <line x1="190" y1="30" x2="190" y2="42" />
      <line x1="260.7" y1="59.3" x2="252.2" y2="67.8" />
      <line x1="290" y1="130" x2="278" y2="130" />
      <line x1="260.7" y1="200.7" x2="252.2" y2="192.2" />
    </g>
    <!-- 72 marker tick -->
    <line x1="235.4" y1="40.9" x2="240.8" y2="30.2" stroke-width="2.2" />
  </g>
  <circle cx="235.4" cy="40.9" r="6" fill="currentColor" stroke="none" />
  <g stroke="none" fill="currentColor" text-anchor="middle">
    <text x="190" y="140" font-size="46" font-weight="700">72</text>
    <text x="190" y="162" font-size="14" opacity="0.8">km/h</text>
    <text x="190" y="185" font-size="13" opacity="0.55">20 m/s</text>
  </g>
</svg>
<figcaption><em>exactly twenty metres a second</em></figcaption>
</figure>

<h2 id="stopping-distance">Stopping distance</h2>

<p>Stopping distance is reaction distance (how far you travel while your brain registers “brake now”) plus braking distance:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>stopping distance = v·t + v² / (2·g·f)
</code></pre></div></div>

<p>With t = 2.5 seconds (the standard cautious value from road design), g rounded to 10, and f = 0.40 for a decent NHAI surface:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>60 km/h  →  76 m
70 km/h  →  96 m
72 km/h  → 100 m
75 km/h  → 106 m
80 km/h  → 117 m
90 km/h  → 141 m
</code></pre></div></div>

<p>Look at 72. Reaction: exactly 50 m. Braking: exactly 50 m. Total: exactly 100 m. I spent ten minutes staring at those numbers, feeling like the second coming of Ramanujan, convinced my vibe was actually a latent mathematical genius.</p>

<p>NH48 is not an expressway, or even a proper highway by definition. It has craters bigger than the moon, crests with enough altitude to spill your coffee, curves blind from all angles, and the occasional cow crossing. It’s not a place to be doing 100 km/h. Past roughly 100 metres of required stopping distance, you’re gambling. That knocks out 85 and above. The first boundary is set: 60 to 80.</p>

<figure class="diagram">
<svg viewBox="0 0 380 128" width="100%" height="auto" role="img" aria-label="A car at the start of a 100-metre track split into two equal 50-metre halves, reaction then braking, stopping exactly at a wall." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
    <!-- road -->
    <line x1="30" y1="74" x2="348" y2="74" stroke-width="2" />
    <!-- stop wall at 100 m -->
    <line x1="348" y1="52" x2="348" y2="92" stroke-width="2.4" />
    <!-- halfway tick at 50 m -->
    <line x1="189" y1="70" x2="189" y2="78" stroke-width="1.4" stroke-dasharray="2 3" opacity="0.6" />
    <!-- segment brackets -->
    <path d="M30 86 V92 H189 V86" stroke-width="1.6" opacity="0.65" />
    <path d="M189 86 V92 H348 V86" stroke-width="1.6" opacity="0.65" />
    <!-- car -->
    <rect x="40" y="52" width="58" height="12" rx="4" stroke-width="1.6" />
    <path d="M52 52 L59 43 L82 43 L90 52" stroke-width="1.6" />
    <circle cx="55" cy="68" r="6" stroke-width="1.6" />
    <circle cx="84" cy="68" r="6" stroke-width="1.6" />
  </g>
  <g stroke="none" fill="currentColor" font-size="10">
    <text x="109" y="108" text-anchor="middle" opacity="0.8">reaction: 50 m</text>
    <text x="268" y="108" text-anchor="middle" opacity="0.8">braking: 50 m</text>
    <text x="348" y="46" text-anchor="middle" font-weight="600">100 m</text>
  </g>
</svg>
<figcaption><em>reaction plus braking, flush at the wall</em></figcaption>
</figure>

<h2 id="small-engines-and-gearing">Small engines and gearing</h2>

<p>Most writing about fuel-efficient cruising speed is American. They talk about peak efficiency near 55 mph, measured on lazy, turbocharged engines with tall gearing. Indian cars are different. My daily driver is a 1.2L, naturally aspirated Swift. The ARAI-rated fuel economy is 23 kmpl, a number produced in a lab by a machine that had nowhere to be. On actual highways, owners report 19 (pre-E20 era) or 17 (post-E20).</p>

<p>What the Team-BHP ownership threads agree on is that a small NA petrol peaks lower than you’d guess, somewhere between 60 and 80 km/h, and starts sagging well before 100. A 1.2-litre engine holding 100 km/h isn’t cruising. It is me at the end of a 10k run: out of oxygen, gasping, and probably at 80 dB if measured. The second boundary: 60 to 78.</p>

<figure class="diagram">
<svg viewBox="0 0 380 180" width="100%" height="auto" role="img" aria-label="A fuel-efficiency curve in kmpl against speed in km/h. Across the whole 60 to 80 band the efficiency drops by barely one kmpl, then falls off past 90." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g fill="none" stroke="currentColor">
    <!-- kmpl gridlines -->
    <g stroke-width="1" stroke-dasharray="2 4" opacity="0.28">
      <line x1="40" y1="56" x2="350" y2="56" />
      <line x1="40" y1="104" x2="350" y2="104" />
    </g>
    <!-- axes -->
    <g stroke-width="1.6" stroke-linecap="round">
      <line x1="40" y1="30" x2="40" y2="132" />
      <line x1="40" y1="132" x2="352" y2="132" />
    </g>
    <!-- efficiency curve -->
    <path d="M46 106 C86 100 108 62 130 58 C168 52 190 54 214 60 C258 68 300 118 350 128" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" />
    <!-- dimension line across the plateau top -->
    <g stroke-width="1.4" stroke-linecap="round" opacity="0.85">
      <line x1="130" y1="44" x2="214" y2="44" />
      <line x1="130" y1="40" x2="130" y2="60" />
      <line x1="214" y1="40" x2="214" y2="62" />
      <path d="M137 41 L130 44 L137 47" />
      <path d="M207 41 L214 44 L207 47" />
    </g>
    <!-- a speech tick to the tail -->
    <line x1="316" y1="96" x2="316" y2="110" stroke-width="1.2" opacity="0.5" />
  </g>
  <g stroke="none" fill="currentColor">
    <g font-size="9" opacity="0.55" text-anchor="end">
      <text x="35" y="59">20</text>
      <text x="35" y="107">15</text>
    </g>
    <text x="30" y="26" font-size="9" opacity="0.7" text-anchor="middle">kmpl</text>
    <g font-size="10" opacity="0.7" text-anchor="middle">
      <text x="46" y="147">40</text>
      <text x="130" y="147">60</text>
      <text x="214" y="147">80</text>
      <text x="298" y="147">100</text>
    </g>
    <text x="352" y="147" font-size="9" opacity="0.6" text-anchor="end">km/h</text>
    <text x="172" y="34" font-size="10" font-weight="600" text-anchor="middle">barely 1 kmpl</text>
    <text x="316" y="90" font-size="10" font-style="italic" opacity="0.7" text-anchor="middle">steep</text>
  </g>
</svg>
<figcaption><em>the whole 60 to 80 band is one flat kmpl</em></figcaption>
</figure>

<h2 id="the-closing-speed-trap">The closing-speed trap</h2>

<p>Previously, my instinct was to cruise well above truck speed so I was never stuck near them. That is backwards. On the busy stretches of NH48, the ones that cause all the braking, trucks settle into 60 to 70. If I’m doing 88 behind a truck doing 65, I’m closing at 23 km/h. The gap vanishes fast and I get a forced decision: brake now or overtake now. That is exactly the ragged driving I am trying to escape.</p>

<p>A small closing speed does the opposite. I gain on the truck slowly, see the decision coming from far back, and half the time the truck’s own gap opens up before I touch the brakes. The target isn’t a safe distance from trucks, it’s a gentle closing speed: 5 to 10 km/h above their band. On empty stretches, trucks do 80 or 90, but overtaking there is easy. The third boundary: 70 to 75.</p>

<figure class="diagram">
<svg viewBox="0 0 380 172" width="100%" height="auto" role="img" aria-label="A filmstrip on a shared clock. Same gap ahead at 0 seconds. Closing at 23 km/h the gap is gone by 12 seconds so you must brake; closing at 7 km/h the gap is still wide open at 12 seconds so you can ease off." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <!-- shared clock labels -->
  <g stroke="none" fill="currentColor" font-size="9" opacity="0.55" text-anchor="middle">
    <text x="70" y="13">0 s</text>
    <text x="190" y="13">6 s</text>
    <text x="310" y="13">12 s</text>
  </g>
  <!-- column separators -->
  <g stroke="currentColor" stroke-width="1" stroke-dasharray="2 4" opacity="0.13">
    <line x1="130" y1="22" x2="130" y2="150" />
    <line x1="250" y1="22" x2="250" y2="150" />
  </g>
  <!-- ROW 1: fast -->
  <g stroke="none" fill="currentColor">
    <text x="20" y="32" font-size="10" opacity="0.7">closing 23 km/h</text>
    <text x="360" y="32" font-size="11" font-weight="700" text-anchor="end">brake now</text>
  </g>
  <line x1="18" y1="66" x2="356" y2="66" stroke="currentColor" stroke-width="1.3" opacity="0.4" />
  <g fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
    <g transform="translate(82,49.5)"><path d="M1 12 L1 3 L19 3 L19 7 L24 7 L27 10 L30 10 L30 12 Z" /><circle cx="6" cy="13.5" r="3" /><circle cx="15" cy="13.5" r="3" /><circle cx="25" cy="13.5" r="3" /></g>
    <g transform="translate(202,49.5)"><path d="M1 12 L1 3 L19 3 L19 7 L24 7 L27 10 L30 10 L30 12 Z" /><circle cx="6" cy="13.5" r="3" /><circle cx="15" cy="13.5" r="3" /><circle cx="25" cy="13.5" r="3" /></g>
    <g transform="translate(322,49.5)"><path d="M1 12 L1 3 L19 3 L19 7 L24 7 L27 10 L30 10 L30 12 Z" /><circle cx="6" cy="13.5" r="3" /><circle cx="15" cy="13.5" r="3" /><circle cx="25" cy="13.5" r="3" /></g>
    <g transform="translate(24,49.5)"><path d="M2 12 L2 9 Q2 8 3 8 L8 8 L11 3.5 L18 3.5 L21 8 L24 8 Q25 8 25 9 L25 12 Z" /><circle cx="7" cy="13.5" r="3" /><circle cx="20" cy="13.5" r="3" /></g>
    <g transform="translate(159,49.5)"><path d="M2 12 L2 9 Q2 8 3 8 L8 8 L11 3.5 L18 3.5 L21 8 L24 8 Q25 8 25 9 L25 12 Z" /><circle cx="7" cy="13.5" r="3" /><circle cx="20" cy="13.5" r="3" /></g>
    <g transform="translate(295,49.5)"><path d="M2 12 L2 9 Q2 8 3 8 L8 8 L11 3.5 L18 3.5 L21 8 L24 8 Q25 8 25 9 L25 12 Z" /><circle cx="7" cy="13.5" r="3" /><circle cx="20" cy="13.5" r="3" /></g>
  </g>
  <!-- ROW 2: slow -->
  <g stroke="none" fill="currentColor">
    <text x="20" y="90" font-size="10" opacity="0.7">closing 7 km/h</text>
    <text x="360" y="90" font-size="11" font-weight="700" text-anchor="end">ease off early</text>
  </g>
  <line x1="18" y1="124" x2="356" y2="124" stroke="currentColor" stroke-width="1.3" opacity="0.4" />
  <g fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
    <g transform="translate(82,107.5)"><path d="M1 12 L1 3 L19 3 L19 7 L24 7 L27 10 L30 10 L30 12 Z" /><circle cx="6" cy="13.5" r="3" /><circle cx="15" cy="13.5" r="3" /><circle cx="25" cy="13.5" r="3" /></g>
    <g transform="translate(202,107.5)"><path d="M1 12 L1 3 L19 3 L19 7 L24 7 L27 10 L30 10 L30 12 Z" /><circle cx="6" cy="13.5" r="3" /><circle cx="15" cy="13.5" r="3" /><circle cx="25" cy="13.5" r="3" /></g>
    <g transform="translate(322,107.5)"><path d="M1 12 L1 3 L19 3 L19 7 L24 7 L27 10 L30 10 L30 12 Z" /><circle cx="6" cy="13.5" r="3" /><circle cx="15" cy="13.5" r="3" /><circle cx="25" cy="13.5" r="3" /></g>
    <g transform="translate(24,107.5)"><path d="M2 12 L2 9 Q2 8 3 8 L8 8 L11 3.5 L18 3.5 L21 8 L24 8 Q25 8 25 9 L25 12 Z" /><circle cx="7" cy="13.5" r="3" /><circle cx="20" cy="13.5" r="3" /></g>
    <g transform="translate(148,107.5)"><path d="M2 12 L2 9 Q2 8 3 8 L8 8 L11 3.5 L18 3.5 L21 8 L24 8 Q25 8 25 9 L25 12 Z" /><circle cx="7" cy="13.5" r="3" /><circle cx="20" cy="13.5" r="3" /></g>
    <g transform="translate(273,107.5)"><path d="M2 12 L2 9 Q2 8 3 8 L8 8 L11 3.5 L18 3.5 L21 8 L24 8 Q25 8 25 9 L25 12 Z" /><circle cx="7" cy="13.5" r="3" /><circle cx="20" cy="13.5" r="3" /></g>
  </g>
</svg>
<figcaption><em>same gap, same clock: at 72 you still have room when 88 has run out</em></figcaption>
</figure>

<h2 id="dividing-by-36">Dividing by 3.6</h2>

<p>Inside 70 to 75, here’s what made me sit up. Converting km/h to m/s means dividing by 3.6, so a speed is a whole number of m/s only if it’s a multiple of 3.6. The nearby multiples of 3.6 in our range: 68.4, 72.0, 75.6. Only one sits inside our acceptable range.</p>

<p>72. Exactly 20 m/s. From there every mid-drive calculation comes out whole: reaction distance 50 m, braking 50 m, two-second gap 40 m, three-second gap 60 m, 100 m every five seconds. Nothing left over anywhere.</p>

<figure class="diagram">
<svg viewBox="0 0 380 205" width="100%" height="auto" role="img" aria-label="A table of mid-drive distances at 70, 72 and 75 km/h. At 72 (20 metres per second) every distance is a whole number of metres; at 70 and 75 they carry ugly decimals." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <!-- highlight behind the 72 column -->
  <rect x="208" y="12" width="64" height="180" rx="8" fill="currentColor" opacity="0.07" />
  <g stroke="none" fill="currentColor" text-anchor="middle">
    <!-- headers -->
    <text x="150" y="30" font-size="13" opacity="0.55">70</text>
    <text x="240" y="30" font-size="17" font-weight="700">72</text>
    <text x="240" y="46" font-size="10" font-weight="600" opacity="0.85">20 m/s</text>
    <text x="330" y="30" font-size="13" opacity="0.55">75</text>
  </g>
  <line x1="16" y1="56" x2="364" y2="56" stroke="currentColor" stroke-width="1" opacity="0.2" />
  <g stroke="none" fill="currentColor">
    <!-- row labels -->
    <g font-size="10" opacity="0.7" text-anchor="end">
      <text x="120" y="82">1 second</text>
      <text x="120" y="110">reaction, 2.5 s</text>
      <text x="120" y="138">two-second gap</text>
      <text x="120" y="166">five seconds</text>
    </g>
    <!-- 70 column (muted, messy) -->
    <g font-size="12" opacity="0.55" text-anchor="middle">
      <text x="150" y="82">19.4</text>
      <text x="150" y="110">48.6</text>
      <text x="150" y="138">38.9</text>
      <text x="150" y="166">97.2</text>
    </g>
    <!-- 72 column (bold, clean) -->
    <g font-size="13" font-weight="700" text-anchor="middle">
      <text x="240" y="82">20</text>
      <text x="240" y="110">50</text>
      <text x="240" y="138">40</text>
      <text x="240" y="166">100</text>
    </g>
    <!-- 75 column (muted, messy) -->
    <g font-size="12" opacity="0.55" text-anchor="middle">
      <text x="330" y="82">20.8</text>
      <text x="330" y="110">52.1</text>
      <text x="330" y="138">41.7</text>
      <text x="330" y="166">104.2</text>
    </g>
    <!-- footer -->
    <text x="240" y="188" font-size="10" font-weight="600" text-anchor="middle">all whole, in metres</text>
  </g>
</svg>
<figcaption><em>the neighbours you could have dialled instead, and their leftovers</em></figcaption>
</figure>

<h2 id="the-compromise">The compromise</h2>

<p>I noticed, partway through, that I took a lot of liberties with the numbers. They are realistic, but I selected them because they were clean and suited my vibe, not because I ran a braking test on NH48.</p>

<p>The three boundaries are mine too. Nudge each by a few km/h and you get a slightly different window. But 72 remains.</p>

<p>I still set the cruise control to 72. Not because of the math, but because at exactly 20 metres a second, the highway stops feeling like a combat zone. The overtaking decisions make themselves, the car doesn’t complain, and I can listen to Kishore Kumar in peace. The math is just the excuse I write down so I don’t have to admit I just want the quiet. But at exactly 20 metres a second, the road stops being a threat. It is just space moving past at a speed I can keep up with.</p>]]></content><author><name>Parth Batra</name></author><category term="i-did-the-math" /><category term="note-to-self" /><summary type="html"><![CDATA[For the last couple of years, I did most of my long drives along NH48, the Delhi-Jaipur Highway, in a Suzuki Swift. Cruise control on, Rafi and Kishore Kumar on the speakers, because that’s the only music that survives four hours of highway without becoming annoying. Over months of nudging the cruise control dial a notch at a time, I kept landing on the same number. Not 70. Not 75. Just 72.]]></summary></entry><entry><title type="html">You Can’t Call It Bad Luck Yet</title><link href="https://fromparth.blog/2026/06/29/you-cant-call-it-bad-luck-yet.html" rel="alternate" type="text/html" title="You Can’t Call It Bad Luck Yet" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-06-29T00:00:00+00:00</updated><id>https://fromparth.blog/2026/06/29/you-cant-call-it-bad-luck-yet</id><content type="html" xml:base="https://fromparth.blog/2026/06/29/you-cant-call-it-bad-luck-yet.html"><![CDATA[<p>This one’s been sitting in my Reddit saved posts for months. I found it on r/philosophy, <a href="https://x.com/batra99/status/2067979140675088702">tweeted it</a> when I first read it, and kept coming back. It’s a parable about a horse.</p>

<p>A man living on the frontier loses his horse. His neighbors gather to sympathize. Bad luck. He shrugs. “Maybe.”</p>

<p>The horse comes back, leading a second, finer horse. The neighbors come back to congratulate him. Good luck. He shrugs. “Maybe.”</p>

<p>His son starts riding the new horse, gets thrown, and breaks his leg. The neighbors come back to mourn his bad fortune. He shrugs. “Maybe.”</p>

<p>Then war breaks out, and every able young man is conscripted and sent to the front. The son, with his broken leg, is left behind.</p>

<p>The parable has a name in Japanese: <em>Saiō ga uma</em>, “the frontier old man’s horse.” It’s older than that, Chinese, around two thousand years old, and it passed into Japanese as a four-character idiom for exactly one thing. You can’t tell yet whether something is good luck or bad. The verdict hasn’t arrived. That’s the whole point of saving it.</p>

<figure class="diagram">
<svg viewBox="0 0 380 200" width="100%" height="auto" role="img" aria-label="The parable as a zigzag: each event flips bad to good to bad to good, while the old man's judgment stays flat at maybe." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
    <line x1="55" y1="100" x2="325" y2="100" stroke-width="1.6" stroke-dasharray="5 5" opacity="0.4" />
    <path d="M55 100 L110 150 L165 60 L220 150 L275 60 L325 110" stroke-width="2.4" />
  </g>
  <circle cx="55" cy="100" r="3.5" fill="currentColor" />
  <g stroke="none" fill="currentColor" font-size="10">
    <text x="110" y="172" text-anchor="middle" opacity="0.7">horse runs</text>
    <text x="110" y="184" text-anchor="middle" opacity="0.55">off</text>
    <text x="165" y="44" text-anchor="middle" opacity="0.7">returns with</text>
    <text x="165" y="56" text-anchor="middle" opacity="0.55">a second horse</text>
    <text x="220" y="172" text-anchor="middle" opacity="0.7">son falls,</text>
    <text x="220" y="184" text-anchor="middle" opacity="0.55">breaks leg</text>
    <text x="275" y="44" text-anchor="middle" opacity="0.7">war comes,</text>
    <text x="275" y="56" text-anchor="middle" opacity="0.55">son stays home</text>
    <text x="342" y="113" opacity="0.7">maybe</text>
    <text x="38" y="97" text-anchor="end" opacity="0.7">maybe</text>
  </g>
</svg>
<figcaption>Every event flips the last verdict. The old man's line never moves.</figcaption>
</figure>

<p>I treated my JEE rank as bad luck for years. Not enough to land a circuit branch at a top IIT, which is the whole point of the exam if you grow up where I did. I went to BITS Pilani instead. A very good college. I knew that then. It didn’t feel like enough.</p>

<p>Here’s the part I missed for a long time. I’d been so fixed on the tag, the letters IIT, that I never asked the only question that mattered: I was going to study that stream for four years. Did I actually want to?</p>

<p>The rank that closed the circuit-branch door is the same rank that left me free to pick the subject I’d quietly been drawn to. Mathematics. The thing I’d have talked myself out of for a better-known name on a sweatshirt. What felt like the worst luck at eighteen turned out to be the moment the choice got handed back to me.</p>

<p>I’m not pretending it was painless at the time. It wasn’t. I sat with it for a while before it turned into anything.</p>

<hr />

<p>The frontier old man’s trick isn’t optimism. He never says it’ll be fine. What he does is refuse to stamp the verdict too early, because the verdict keeps moving. The neighbors keep showing up with their condolences and their congratulations, and every single time, they’re wrong about something.</p>

<p>I still catch myself calling things bad luck the week they happen. A rejected application, a plan that collapses. I’m the neighbor. I want to know <em>now</em>. The parable says I can’t, and I’m starting to believe it.</p>

<p>The horse ran away. Maybe.</p>

<hr />

<p><em>The bad luck was just the luck, a few years early.</em></p>]]></content><author><name>Parth Batra</name></author><category term="just-a-thought" /><category term="note-to-self" /><summary type="html"><![CDATA[This one’s been sitting in my Reddit saved posts for months. I found it on r/philosophy, tweeted it when I first read it, and kept coming back. It’s a parable about a horse.]]></summary></entry><entry><title type="html">I Wrote 15 Postcards Nobody Asked For. They Weren’t for My Friends.</title><link href="https://fromparth.blog/2026/06/22/i-started-doing-the-last-5-percent-of-everything.html" rel="alternate" type="text/html" title="I Wrote 15 Postcards Nobody Asked For. They Weren’t for My Friends." /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>https://fromparth.blog/2026/06/22/i-started-doing-the-last-5-percent-of-everything</id><content type="html" xml:base="https://fromparth.blog/2026/06/22/i-started-doing-the-last-5-percent-of-everything.html"><![CDATA[<p>I wrote 15 postcards from the highest post office in the world. It’s in Hikkim, Spiti, somewhere around 15000 feet. I was there some time back, and I spent a full day writing to the people close to me at that time. By hand, each one different, the kind of penmanship I normally save for signing official documents.</p>

<p>Most came back with a “thanks.” A few came back with “love you bro.” If you know any emotionally unavailable mid-20s Indian men, you know that’s the highest compliment physically possible. We are a nonchalant final boss. “Love you bro” from us is a Nobel acceptance speech.</p>

<p>Here’s what I keep turning over though. If I’d done the formality version, the postcard equivalent of a forwarded meme, I’d probably have gotten the same replies. “Thanks man” comes free. The “love you bro” might have come anyway.</p>

<p>The extra effort wasn’t what earned the reactions. They would have come anyway. But here’s the part I didn’t expect. Sitting down to write fifteen postcards by hand, one full day, each one actually different, I felt <em>proud</em>. Not of the postcards. Of the version of me who chose to do it that way. And when the “love you bro”s came back, I felt loved, and appreciated, in a way a forwarded meme has never made anyone feel.</p>

<p>That good feeling is the part nobody talks about. And it’s the whole point.</p>

<figure class="diagram">
<svg viewBox="0 0 380 150" width="100%" height="auto" role="img" aria-label="Four small doodles: a handwritten postcard, a chat bubble with three dots, a photo with a star review, and a dumbbell." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g transform="translate(50,75)" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <rect x="-20" y="-13" width="40" height="26" rx="2" />
    <line x1="-4" y1="-13" x2="-4" y2="13" />
    <rect x="8" y="-10" width="9" height="7" />
    <line x1="0" y1="2" x2="16" y2="2" />
    <line x1="0" y1="7" x2="13" y2="7" />
    <path d="M-15 -7 q3 -3 6 0 q3 3 6 0" />
    <path d="M-15 0 q3 -3 6 0 q3 3 6 0" />
  </g>
  <g transform="translate(145,75)" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <path d="M-16 -10 h32 a4 4 0 0 1 4 4 v12 a4 4 0 0 1 -4 4 h-20 l-6 5 v-5 h-6 a4 4 0 0 1 -4 -4 v-12 a4 4 0 0 1 4 -4 z" />
    <circle cx="-6" cy="0" r="1.6" fill="currentColor" stroke="none" />
    <circle cx="0" cy="0" r="1.6" fill="currentColor" stroke="none" />
    <circle cx="6" cy="0" r="1.6" fill="currentColor" stroke="none" />
  </g>
  <g transform="translate(240,75)" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <rect x="-16" y="-6" width="22" height="16" rx="1.5" />
    <circle cx="-11" cy="0" r="2.2" fill="currentColor" stroke="none" />
    <path d="M-16 7 l7 -6 l5 4 l4 -3 l6 5" />
    <path d="M10 -10 l1.4 3 l3.2 .4 l-2.3 2.2 l.6 3.2 l-2.9 -1.6 l-2.9 1.6 l.6 -3.2 l-2.3 -2.2 l3.2 -.4 z" fill="currentColor" stroke="none" />
  </g>
  <g transform="translate(335,75)" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
    <rect x="-23" y="-9" width="8" height="18" rx="1.5" />
    <rect x="-20" y="-12" width="5" height="24" rx="1.5" />
    <rect x="15" y="-9" width="8" height="18" rx="1.5" />
    <rect x="12" y="-12" width="5" height="24" rx="1.5" />
    <line x1="-15" y1="0" x2="12" y2="0" />
  </g>
</svg>
<figcaption>A postcard nobody asked for. A message to a deal that fell through. A review with photos. A set finished at full depth. None of it moves the task. All of it moves the operator.</figcaption>
</figure>

<h2 id="the-last-5">The last 5%</h2>

<p>I’ve started noticing a part of every task that nobody asks for and almost nobody does. I’ve been calling it the last 5%. It doesn’t move the outcome. It moves everything else.</p>

<p>It showed up loudest in arguments. I’m competitive on the inside, not the loud kind, the table-tennis-at-zonal-level kind, still wired somewhere to win the point. So I wasn’t really listening, I was loading my rebuttal while the other person was still talking. The shift only came when I let myself see that the people arguing with me were arguing <em>for</em> my betterment.</p>

<p>A few more, from the last few months. A travel agent I went back and forth with for days over a Spiti trip, until the deal fell through. The easy thing was to ghost. He’s a stranger on the internet, the transaction’s dead, why bother. I sent three sentences instead: thanks for the time, here’s why it didn’t fit, ping me if a better window opens. Forty seconds. Changed nothing about the trip.</p>

<p>In Spiti itself I stayed at small homestays where people had bet everything on the place. After checkout the easy thing is to vanish, so I sat down and wrote detailed reviews, with photos, for every stay and every cafe owner who was kind to me. The woman who served me hot chocolate and a brownie will probably never connect the review to the quiet guy in the corner.</p>

<h2 id="what-i-think-is-actually-happening">What I think is actually happening</h2>

<p>Here’s the thing I keep turning over. None of these improved the task. The postcards didn’t make me a better friend in that moment. The travel agent message didn’t get me a cheaper trip. The reviews didn’t earn me a discount.</p>

<p>What they did was cast a vote. Each one says: I’m the kind of person who finishes things properly, even when nobody’s watching, even when it doesn’t matter. And votes accumulate. One is worth nothing. A hundred is a different person.</p>

<p>Doing things well, as far as I can tell, isn’t a quality strategy. It’s an identity strategy. The point of the well-written message isn’t a better message. It’s that you become someone who writes messages like that, and that person writes better, codes better, lifts better, listens better, shows up better, because they’re not a different person in each room. The standard travels with you.</p>

<p>I used to think I had separate dials. Work at 90, gym at 40, relationships at 60, dishes at 20. I assumed they were independent. Lately I’m not so sure. If my morning gym session was phoned in, I don’t feel right the rest of the day. Something’s off and I can’t quite name it. And no matter how good the food is, a meal eaten out of a badly-arranged, dirty kitchen doesn’t taste good to me. The low standard in one corner contaminates the felt quality of everything else. The standard isn’t parcelled out by room. It’s one dial. And I can’t quarantine the low settings. They bleed into how the whole day sits in my body.</p>

<hr />

<p>This isn’t “do everything perfectly.” Perfectionism is a different disease, and it ships nothing because nothing is ever ready. The last 5% thing is the opposite. It’s about finishing, properly, and moving on. Perfectionism says <em>it’s not ready</em>. The last 5% says <em>it’s ready, I did it right, next</em>.</p>

<p>The test I’ve been running on myself: could I find one more minute to make this better, and would future-me be glad I spent it? Usually yes. Sometimes no, and then I ship. I’m still figuring out which is which. I default to yes because I haven’t earned the right to say no yet. The people who default to no aren’t being efficient, I think. They’re just practicing a different identity, and getting very good at it.</p>

<p>I don’t do any of this because you’ll notice. You won’t, most of the time. I do it because of that feeling in Hikkim. The postcards weren’t for the replies. They were for the version of me who sat down and wrote them, and <em>that version is the one I’m stuck with for the rest of my life.</em></p>

<p>There’s an old line, i think it comes from Zen, <em>How you do anything is how you do everything.</em> It used to sound like a threat. Now it sounds like the most generous thing I’ve ever read. It means the postcard matters. The gym set matters. The message to the travel agent matters. Google review matters. Not because the world is keeping score, but because you are always, in every one of them, rehearsing and becoming someone. You might as well become someone who does things right!</p>]]></content><author><name>Parth Batra</name></author><category term="note-to-self" /><category term="tiny-reps" /><summary type="html"><![CDATA[I wrote 15 postcards from the highest post office in the world. It’s in Hikkim, Spiti, somewhere around 15000 feet. I was there some time back, and I spent a full day writing to the people close to me at that time. By hand, each one different, the kind of penmanship I normally save for signing official documents.]]></summary></entry><entry><title type="html">I Was Confidently Wrong About Almost Everything</title><link href="https://fromparth.blog/2026/06/22/i-was-confidently-wrong-about-almost-everything.html" rel="alternate" type="text/html" title="I Was Confidently Wrong About Almost Everything" /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>https://fromparth.blog/2026/06/22/i-was-confidently-wrong-about-almost-everything</id><content type="html" xml:base="https://fromparth.blog/2026/06/22/i-was-confidently-wrong-about-almost-everything.html"><![CDATA[<p>My brain has been lying to me with confidence my entire life. Not occasionally. Constantly. And for a long time I didn’t notice, because confidence is the one thing that feels exactly the same whether it’s earned or not.</p>

<p>I spent a while going after this. Reading, catching myself mid-decision, writing down the places I’d been confidently wrong. The first thing I learned is that the lies aren’t random. They come in patterns. Once you see them, you can’t unsee them.</p>

<figure class="diagram">
<svg viewBox="0 0 380 150" width="100%" height="auto" role="img" aria-label="A decision path winding forward with three traps on it: a misdirected spotlight, a dragging anchor, and tinted glasses." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
    <path d="M28 78 Q120 60 192 78 T352 72" stroke-width="2.2" />
    <path d="M346 66 L352 72 L346 78" stroke-width="2" />
    <path d="M84 74 L62 34 L106 34 Z" stroke-width="1.8" opacity="0.6" />
    <line x1="84" y1="34" x2="84" y2="22" stroke-width="1.8" />
    <circle cx="84" cy="18" r="4" stroke-width="1.8" opacity="0.6" />
    <path d="M188 78 L172 78" stroke-width="1.8" />
    <path d="M172 78 V100" stroke-width="1.8" />
    <path d="M162 100 C162 110 182 110 182 100" stroke-width="1.8" />
    <line x1="172" y1="100" x2="172" y2="108" stroke-width="1.8" />
    <circle cx="294" cy="74" r="11" stroke-width="1.8" opacity="0.65" />
    <circle cx="318" cy="74" r="11" stroke-width="1.8" opacity="0.65" />
    <line x1="305" y1="74" x2="307" y2="74" stroke-width="1.5" />
    <line x1="282" y1="74" x2="284" y2="74" stroke-width="1.5" />
  </g>
</svg>
<figcaption>Three traps. Each one a different way my brain lied to me.</figcaption>
</figure>

<p>Here are three of its favourite lies.</p>

<hr />

<h2 id="bias-1-survivorship-bias-your-brain-only-counts-the-winners">Bias #1: Survivorship bias, your brain only counts the winners</h2>

<p>Steve Jobs did it. Zuckerberg did it. Gates did it. Dropouts, all of them, and your brain connects the dots: dropping out is the move.</p>

<p>What it doesn’t connect is the millions who dropped out and now make less than their graduating classmates. Nobody writes a magazine profile about that guy.</p>

<p>But here’s the half of survivorship bias nobody talks about. It’s not just the failures you don’t see. It’s most of the successes too.</p>

<p>I work at a company called Procol. Real business, real revenue. You’ve never heard of it. You wouldn’t recognise the founders if they were standing next to you in a lift.</p>

<p>The founders everyone can name are famous for being famous, not for building the most. The actual economy runs on companies you’ve never heard of, and my brain keeps pointing at the famous three and calling that the data.</p>

<p>I catch myself doing the same thing with startup founders. I’ll read about someone who quit their FAANG job, built a SaaS, and hit $1M ARR in 18 months, and my brain says “that could be me.” What it doesn’t say: for every one of those stories, thousands quit, burned through savings, and quietly went back to job hunting. I just never meet them, because they don’t post about it on LinkedIn.</p>

<p>The media doesn’t report reality. It reports interesting deviations from reality. And my brain treats those deviations as the norm.</p>

<hr />

<h2 id="bias-2-sunk-cost-fallacy-throwing-good-after-bad">Bias #2: Sunk cost fallacy, throwing good after bad</h2>

<p>The clean version first. You buy a movie ticket for $20. Fifteen minutes in, you know it’s garbage. Do you leave?</p>

<p>Of course not. You sit there for another two and a half hours because you already paid. Here’s the math your brain refuses to do: that $20 is gone whether you stay or go. By staying, you’re spending more time on top of it. If your time’s worth $20 an hour, you just spent $50 to “recover” $20. A $20 mistake now costs you $70, and $50 of that was voluntary.</p>

<p>This is the one bias I’m actually good at, and it shows up in the smallest place. I start a book. A hundred pages in, it’s not getting better, or it’s just not my taste, for a thousand different reasons. I drop it. Why spend any more time on it. The $20 is gone. Walk out.</p>

<p>Books are easy, because the cost is small and the exit is clean.</p>

<p>Then there’s the version you can’t price. Relationships. You can’t put a number on emotions, which makes sunk cost fallacy almost impossible to see in real time. You’ve invested six months, a year, three years. You know it’s not going anywhere. But leaving means all that time was “wasted,” so you invest another year. And another.</p>

<p>The thing I learned, the hard way, is that familiarity and love are two different things. They feel similar enough that you can spend years confusing one for the other. The math is identical to the movie ticket: the cost is already sunk, the only question is whether you keep paying. Except you can’t see it, because there’s no receipt.</p>

<hr />

<h2 id="bias-3-confirmation-bias-seeing-what-you-already-believe">Bias #3: Confirmation bias, seeing what you already believe</h2>

<p>I was watching vlogs by Paramvir Beniwal, a travel YouTuber who’s been to over a hundred countries. One of them was from a village in Africa, and the thing that hit me was how much it looked like a village in a poor part of India. Same kind of houses, same kind of dust, same kind of people getting on with their day.</p>

<p>So I looked it up. Per capita income in that part of the world is in the same band as Bihar, around $880 a year. Girls’ education, for every ten years boys spend in school, girls spend about nine. The numbers weren’t the catastrophe I’d been carrying around in my head.</p>

<p>Here’s what I realised about my own head. I’d absorbed one Africa, the one on the news, which only shows up for famines, wars, and coups. My brain had filed the whole continent under “uniformly terrible,” and every headline confirmed it. A travel vlog was the first time I’d looked at the same place from a different angle, and the picture wouldn’t hold.</p>

<p>That’s confirmation bias. Not a bad dataset, but a dataset you only ever look at from one side. Every fact that fits, you nod at. Every fact that doesn’t, you find a reason to dismiss.</p>

<p>There’s a line in <em>Factfulness</em> that lands differently now. The authors describe couples across the United States, Iran, Mexico, Malaysia, Brazil, Italy, China, Indonesia, India, Colombia, Bangladesh, South Africa (different languages, different scriptures on the nightstand) and they’re all doing the same thing in their bedrooms. Whispering about their dreams for their kids’ future.</p>

<p>Every language has a word for “happy.” Every language has a word for “grief.” That’s not a coincidence. It’s evidence that humans run on nearly identical hardware, and most of what we think divides us is just what we haven’t looked at closely enough.</p>

<p>I’m not saying the world is fine. It isn’t fine in the village I saw in that vlog, and it isn’t fine in Bihar either. But the world isn’t as bad as the news needs it to be. It’s also not as good, everywhere, in uniform. It’s somewhere in between, and you only find where by actually looking.</p>

<hr />

<h2 id="so-what-do-i-do-with-this">So what do I do with this?</h2>

<p>I don’t have a cure. The brain still lies. But I’ve got three questions I run on myself now, one per lie.</p>

<p><strong>When a success story sounds too clean:</strong> how many people tried the same thing and failed, and how many succeeded without anyone noticing?</p>

<p><strong>When I’m holding on because I’ve “already invested so much”:</strong> if I were starting fresh today, would I make the same choice?</p>

<p><strong>When a belief feels obviously true:</strong> what data would change my mind?</p>

<p>If I can’t answer that last one, I’m not thinking. I’m just confirming.</p>]]></content><author><name>Parth Batra</name></author><category term="brain-glitch" /><category term="note-to-self" /><category term="just-a-thought" /><summary type="html"><![CDATA[My brain has been lying to me with confidence my entire life. Not occasionally. Constantly. And for a long time I didn’t notice, because confidence is the one thing that feels exactly the same whether it’s earned or not.]]></summary></entry><entry><title type="html">The People Who Make You Better Just by Existing</title><link href="https://fromparth.blog/2026/06/15/the-people-who-make-you-better-just-by-existing.html" rel="alternate" type="text/html" title="The People Who Make You Better Just by Existing" /><published>2026-06-15T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://fromparth.blog/2026/06/15/the-people-who-make-you-better-just-by-existing</id><content type="html" xml:base="https://fromparth.blog/2026/06/15/the-people-who-make-you-better-just-by-existing.html"><![CDATA[<p>This month, a single line from <a href="https://dailystoic.com/"><em>The Daily Stoic</em></a> stopped me cold:</p>

<blockquote>
  <p>“Happy is the person who can improve others not only when present but even in their thoughts.”</p>
</blockquote>

<p>Read that again. There are people who make us better when they’re around. Then there are people who make us better just by existing in our memory. You’re about to do something you wouldn’t want them to see, and their face pops into your head, and you stop.</p>

<figure class="diagram">
<svg viewBox="0 0 380 160" width="100%" height="auto" role="img" aria-label="A path forks into a worse branch and a better branch; a dotted thought of someone nudges the choice toward the better branch." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
    <path d="M30 115 H150" stroke-width="2.2" />
    <path d="M150 115 L320 55" stroke-width="2.2" />
    <path d="M150 115 L320 145" stroke-width="1.8" opacity="0.35" />
  </g>
  <circle cx="150" cy="115" r="3.5" fill="currentColor" />
  <g fill="none" stroke="currentColor" stroke-width="1.6" stroke-dasharray="3 3" opacity="0.6">
    <path d="M150 115 C 150 70, 200 52, 228 48" />
    <circle cx="244" cy="46" r="15" />
  </g>
  <g stroke="none" fill="currentColor" font-size="13">
    <text x="244" y="50" text-anchor="middle" opacity="0.75">them</text>
    <text x="324" y="55">better</text>
    <text x="324" y="149" opacity="0.5">worse</text>
  </g>
</svg>
<figcaption>The thought of one person you respect can nudge a choice down the better fork.</figcaption>
</figure>

<p>It hit me hard because I’ve felt this. Last week I was fighting with my younger brother over text. He’s five years younger, and somewhere along the way he built a picture of me that’s better than I usually am. I had a reply ready that would’ve won the argument and cost me a piece of that. Right before I sent it, his face came to mind, and I deleted it. Not because he’d catch me or punish me. He can’t do either. I’m older (and much stronger!). Because I wanted to be who he already thinks I am.</p>

<p>It’s not the same as religion’s universal watcher concept, where fear of being seen keeps you in line. Fear makes you hide the worst of you. Love makes you reach for the best. Same watcher, different engine.</p>

<p>The Stoic who wrote that line called it happiness. I’d call it borrowing a face you trust until your own grows into it.</p>]]></content><author><name>Parth Batra</name></author><category term="just-a-thought" /><category term="note-to-self" /><summary type="html"><![CDATA[This month, a single line from The Daily Stoic stopped me cold:]]></summary></entry><entry><title type="html">Measuring Personal Growth</title><link href="https://fromparth.blog/2026/06/12/how-i-measure-personal-growth.html" rel="alternate" type="text/html" title="Measuring Personal Growth" /><published>2026-06-12T00:00:00+00:00</published><updated>2026-06-12T00:00:00+00:00</updated><id>https://fromparth.blog/2026/06/12/how-i-measure-personal-growth</id><content type="html" xml:base="https://fromparth.blog/2026/06/12/how-i-measure-personal-growth.html"><![CDATA[<p>During table tennis practice last year, I caught myself counting imaginary scores. Not the actual rally. The <em>implied</em> one. This person broke it five times, I broke it three, therefore I’m losing. At practice. Against friends. No stakes whatsoever. I’d done it my whole life: hackathons, group projects, anything with a scoreboard real or invented. If I was in, I was in to win.</p>

<p>I don’t count imaginary scores anymore. Sometimes I lose a rally on purpose now, just to keep the game going, because the game is the point. Noticing that shift is what got me thinking about how you’d even measure something like personal growth in the first place. If I’d quietly gotten better at the one thing I’d been bad at my whole life, what else could you actually track? And who else thinks about this?</p>

<p>Some friends told me this whole exercise is mildly sociopathic. Why measure everything? Life is to be lived, not scored. They’re not wrong, they’re just asking the wrong person to stop. I’m a mathematician and an athlete at the core. I will always keep score. I will always want the win. I’m going to measure, whether I decide to or not. So the question was never whether to measure. It was <em>what</em> to measure. Net worth and follower counts aren’t what I live for. This post is me trying to point that instinct at the right numbers. It’s me trying to mend me.</p>

<h2 id="rate-of-change">Rate of change</h2>

<p>I have this theory that life has a circadian rhythm. Every 3-6 years, you become a different person. You work on different problems. Your lifestyle changes. The people you hang out with are different. If you haven’t caught up with a friend in 5 years, you might no longer have anything in common. It’s not a coincidence that schools are structured into chunks of 3-6 years.</p>

<p>I can see it in my own life, chapter by chapter. Table tennis. Competitive maths. Engineering school. Each one a different person, each one barely recognizing the last. And then, for almost two years, I was nobody in particular. Between versions, with no next one in sight. I’d come out of a structure that told me who I was every morning, and for the first time nobody was handing me one. I didn’t handle it well. It was only recently that I felt like I had a handle on life again.</p>

<p>I wonder if I could treat myself like an investment, and measure my growth by how long it’d take me to become a new person.</p>

<p>Finance has a name for this. The rule of 72. Divide 72 by your annual return and you get the years it takes to double. 8% a year, and your money doubles every nine. What I want to know is the return on me. How long until the version I’m living as now is unrecognizable to the one who started?</p>

<p>Becoming a new person isn’t always a good thing, and probably not the goal for everyone. But for me, it is. I want to see things from a new perspective. I want to be exposed to new challenges.</p>

<p>Becoming a new person sounds clean. It isn’t. It feels like outgrowing a room you used to love, walking back in years later, and finding the ceiling too low to stand up straight in. You can’t un-grow. Some of my oldest friends are rooms I can’t stand in anymore. We still talk, I still love them, but I catch myself performing the old version of me so the conversation has somewhere to land, and we both feel the draft coming through the gap. The growth and the loss are the same event. I haven’t figured out how to want one without getting the other.</p>

<figure class="diagram">
<svg viewBox="0 0 720 230" role="img" aria-label="A horizontal strip of five life chapters shown as distinct blocks across time. From left to right: table tennis, competitive maths, engineering school, a hatched gap of roughly two years labelled with a question mark, and now, drawn in green. Each chapter is a different person. The gap is the time between versions, where none of them fit.">
  <!-- chapter 1: table tennis -->
  <rect x="40" y="70" width="120" height="56" rx="4" fill="currentColor" opacity="0.13" />
  <text x="100" y="102" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="currentColor" opacity="0.7">table tennis</text>
  <text x="100" y="146" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="11" fill="currentColor" opacity="0.4">grade 3 to 9</text>

  <!-- chapter 2: competitive maths -->
  <rect x="168" y="70" width="96" height="56" rx="4" fill="currentColor" opacity="0.18" />
  <text x="216" y="98" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="currentColor" opacity="0.7">competitive</text>
  <text x="216" y="114" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="currentColor" opacity="0.7">maths</text>
  <text x="216" y="146" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="11" fill="currentColor" opacity="0.4">~3 yrs</text>

  <!-- chapter 3: engineering school -->
  <rect x="272" y="70" width="150" height="56" rx="4" fill="currentColor" opacity="0.13" />
  <text x="347" y="98" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="currentColor" opacity="0.7">engineering</text>
  <text x="347" y="114" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="currentColor" opacity="0.7">school</text>
  <text x="347" y="146" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="11" fill="currentColor" opacity="0.4">~5 yrs</text>

  <!-- the gap: hatched, no label -->
  <defs>
    <pattern id="gap-hatch" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45)">
      <line x1="0" y1="0" x2="0" y2="8" stroke="currentColor" stroke-width="1.4" opacity="0.35" />
    </pattern>
  </defs>
  <rect x="430" y="70" width="80" height="56" rx="4" fill="url(#gap-hatch)" stroke="currentColor" stroke-width="1" stroke-dasharray="3 3" opacity="0.55" />
  <text x="470" y="104" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="20" fill="currentColor" opacity="0.4">?</text>
  <text x="470" y="146" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="11" fill="currentColor" opacity="0.4">~2 yrs</text>

  <!-- chapter 5: now, green -->
  <rect x="518" y="70" width="160" height="56" rx="4" fill="#56b947" opacity="0.18" />
  <rect x="518" y="70" width="160" height="56" rx="4" fill="none" stroke="#56b947" stroke-width="1.5" opacity="0.7" />
  <text x="598" y="98" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="#56b947">a handle</text>
  <text x="598" y="114" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="#56b947">on life</text>
  <text x="598" y="146" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="11" fill="#56b947" opacity="0.75">now</text>

  <!-- caption inside svg: the discontinuity -->
  <text x="360" y="195" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="12.5" fill="currentColor" opacity="0.5" font-style="italic">each block is a different person. the hatched one is where none of them fit.</text>
</svg>
<figcaption>My own chapters. Five people so far, and a gap where there wasn’t one.</figcaption>
</figure>

<h2 id="time-to-solve-problems">Time to solve problems</h2>

<p>Mayank, an old friend doing his post-doc at Cambridge, frames it like this. Most people walk into three big problems after college, and spend roughly a decade on each. First, figuring out what you want to do with your life. Then marriage, a house, kids. Then savings, and the slow climb toward retirement. He calls them life projects. His goal is to close his faster, so he can get to more interesting ones. I realized I’d been quietly running the same race without ever naming it.</p>

<p>This made me think that perhaps I can measure my growth by looking at what big problems I’ve solved. What big problems was I worried about 5 years ago that I no longer worry about now? What big problems am I worried about now that I don’t want to worry about in 5 years?</p>

<p>What counts as a big problem depends on the person. For me, it’s career, finance, family, and health.</p>

<p>Five years ago the college kid in me panicked about money constantly. Every UPI notification was a small heart attack. That went quiet the day there was finally a real safety net behind me, and I didn’t notice the silence until months later. I used to want a place that was actually mine, in a big city, actually safe. That’s sorted now. My parents’ health used to send me into a spin with every little niggle. The niggles still come. I’ve just gotten better at carrying them without letting them flatten the whole week.</p>

<p>Some things quietly stopped registering as threats at all. Performance reviews used to cost me three days of low-grade dread each. Now they don’t cost me much. I didn’t fix that on purpose. I looked up one day and the fear had moved out without telling me.</p>

<figure class="diagram">
<svg viewBox="0 0 920 300" role="img" aria-label="A vertical stack of five worries. From top to bottom: money is ghosted and faint, with an arrow labelled went quiet. A place of my own is ghosted and faint. Parents' health is half-faded. Performance reviews are ghosted and faint. What do I actually want to do is drawn in solid green with an open ring, still unresolved.">
  <!-- column header -->
  <text x="450" y="28" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="currentColor" opacity="0.5" font-style="italic">five years of worries, and what happened to them</text>

  <!-- row 1: money (ghosted) -->
  <text x="220" y="72" text-anchor="end" font-family="Source Serif 4, Georgia, serif" font-size="14" fill="currentColor" opacity="0.3">money</text>
  <line x1="235" y1="68" x2="600" y2="68" stroke="currentColor" stroke-width="1.4" opacity="0.18" stroke-dasharray="2 5" />
  <text x="615" y="72" font-family="Source Serif 4, Georgia, serif" font-size="12" fill="currentColor" opacity="0.4" font-style="italic">went quiet</text>

  <!-- row 2: a place of my own (ghosted) -->
  <text x="220" y="112" text-anchor="end" font-family="Source Serif 4, Georgia, serif" font-size="14" fill="currentColor" opacity="0.3">a place that's mine</text>
  <line x1="235" y1="108" x2="600" y2="108" stroke="currentColor" stroke-width="1.4" opacity="0.18" stroke-dasharray="2 5" />
  <text x="615" y="112" font-family="Source Serif 4, Georgia, serif" font-size="12" fill="currentColor" opacity="0.4" font-style="italic">sorted</text>

  <!-- row 3: parents' health (half-faded) -->
  <text x="220" y="152" text-anchor="end" font-family="Source Serif 4, Georgia, serif" font-size="14" fill="currentColor" opacity="0.55">parents' health</text>
  <line x1="235" y1="148" x2="600" y2="148" stroke="currentColor" stroke-width="1.6" opacity="0.4" />
  <text x="615" y="152" font-family="Source Serif 4, Georgia, serif" font-size="12" fill="currentColor" opacity="0.45" font-style="italic">still here, easier to carry</text>

  <!-- row 4: performance reviews (ghosted) -->
  <text x="220" y="192" text-anchor="end" font-family="Source Serif 4, Georgia, serif" font-size="14" fill="currentColor" opacity="0.3">performance reviews</text>
  <line x1="235" y1="188" x2="600" y2="188" stroke="currentColor" stroke-width="1.4" opacity="0.18" stroke-dasharray="2 5" />
  <text x="615" y="192" font-family="Source Serif 4, Georgia, serif" font-size="12" fill="currentColor" opacity="0.4" font-style="italic">moved out</text>

  <!-- row 5: what do I actually want to do (solid green, open) -->
  <text x="220" y="238" text-anchor="end" font-family="Source Serif 4, Georgia, serif" font-size="14" fill="#56b947" font-weight="500">what do I actually want to do</text>
  <circle cx="245" cy="234" r="6" fill="none" stroke="#56b947" stroke-width="1.8" />
  <line x1="260" y1="234" x2="600" y2="234" stroke="#56b947" stroke-width="2" opacity="0.85" />
  <text x="615" y="238" font-family="Source Serif 4, Georgia, serif" font-size="12" fill="#56b947" font-style="italic">still open</text>

  <!-- footer note -->
  <text x="450" y="280" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="12.5" fill="currentColor" opacity="0.5" font-style="italic">growth isn't the new problems you take on. it's the old ones that stop costing you.</text>
</svg>
<figcaption>The ones that went quiet. The one that didn’t.</figcaption>
</figure>

<hr />

<p>The big one is still open: what I actually want to do long-term, and where. My own company? A move to the US or Europe? That’s the weight I’d like to not be carrying five years from now. Writing that sentence down, in public, is the most honest thing in this post, because now I can’t pretend I haven’t noticed it.</p>

<h2 id="number-of-future-options">Number of future options</h2>

<p>A while back I read a theory on Twitter that every few years, half of your dreams die. People give up on their dreams because they realize they can no longer achieve them.</p>

<p>I disagree. As I grow older, I have more dreams. I now know many things that I didn’t know before, and I have access to more resources than I ever did. This allows me to do things that I used to think of as impossible. And the dreams keep multiplying instead of dying off. I keep a running <a href="/list-100/">list of a hundred things I want to do</a>, and the giveaway is that I’ve crossed things off it and then deleted them anyway, swapping a goal I’d already hit for a bigger one. I can’t sit still, even with a win in hand.</p>

<p>During a reinforcement learning course in college, I learned there’s a name for how I’d already been living. <em>Empowerment maximization.</em> When an agent faces uncertainty, it picks the move that keeps the most doors open. Faced with a wall of switches, it pulls the one that unlocks the most others.</p>

<p>I didn’t need the jargon. I’d been doing it for years without a word for it. In the face of uncertainty, I lean towards the decision that gives me the most future options. I’d choose a job that pays less if it buys me more optionality: open source, papers, transferable skills over niche ones. I’d rather earn less now and keep the doors open behind me.</p>

<p>Perhaps I can measure my growth by how many new options I have gained/lost. What options are available to me today that were not available to me 5 years ago? What options were available to me 5 years ago that aren’t available to me now? More importantly, what options that are not available to me today do I want 5 years from now?</p>

<p>I came across this image from Wait But Why. As time goes by, many doors close to us, but many new ones open up. That tweet was talking about the red lines on the left. I keep my eyes on the green lines on the right.</p>

<figure class="diagram">
<svg viewBox="0 0 960 560" role="img" aria-label="A diagram in the style of Wait But Why. On the left, many red life paths fan out from a single birth point through the past and converge at today. On the right, green life paths open out from today and branch widely into the future. Red paths are closed to you; green paths are open.">
  <path d="M 60.0 270.0 C 64.3 263.6 77.1 244.3 85.6 231.5 C 94.2 218.7 102.7 205.7 111.2 193.1 C 119.8 180.4 128.3 165.7 136.9 155.7 C 145.4 145.7 154.0 142.8 162.5 133.1 C 171.0 123.4 179.6 105.8 188.1 97.7 C 196.7 89.7 205.2 88.7 213.8 84.8 C 222.3 81.0 230.8 77.5 239.4 74.4 C 247.9 71.3 256.5 65.1 265.0 66.3 C 273.5 67.6 282.1 78.2 290.6 81.7 C 299.2 85.3 307.7 84.9 316.2 87.6 C 324.8 90.4 333.3 91.2 341.9 98.1 C 350.4 105.0 359.0 118.7 367.5 129.0 C 376.0 139.2 384.6 148.8 393.1 159.5 C 401.7 170.2 410.2 181.5 418.8 193.3 C 427.3 205.2 435.8 217.9 444.4 230.6 C 452.9 243.4 465.7 263.4 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 264.4 77.1 247.0 85.6 236.6 C 94.2 226.1 102.7 215.7 111.2 207.1 C 119.8 198.4 128.3 193.7 136.9 184.5 C 145.4 175.4 154.0 161.3 162.5 152.2 C 171.0 143.1 179.6 136.3 188.1 129.8 C 196.7 123.3 205.2 115.7 213.8 113.1 C 222.3 110.4 230.8 115.0 239.4 114.0 C 247.9 113.0 256.5 106.5 265.0 107.0 C 273.5 107.4 282.1 115.2 290.6 116.9 C 299.2 118.5 307.7 114.2 316.2 116.9 C 324.8 119.5 333.3 127.0 341.9 132.8 C 350.4 138.6 359.0 143.6 367.5 151.7 C 376.0 159.9 384.6 171.7 393.1 181.6 C 401.7 191.5 410.2 201.2 418.8 211.1 C 427.3 220.9 435.8 230.9 444.4 240.7 C 452.9 250.6 465.7 265.1 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 266.1 77.1 254.3 85.6 246.8 C 94.2 239.3 102.7 232.7 111.2 225.0 C 119.8 217.4 128.3 208.0 136.9 201.0 C 145.4 194.0 154.0 189.3 162.5 182.8 C 171.0 176.4 179.6 168.1 188.1 162.1 C 196.7 156.1 205.2 147.1 213.8 146.8 C 222.3 146.5 230.8 159.6 239.4 160.1 C 247.9 160.6 256.5 153.2 265.0 149.9 C 273.5 146.5 282.1 138.5 290.6 140.1 C 299.2 141.6 307.7 153.3 316.2 159.4 C 324.8 165.5 333.3 173.9 341.9 176.7 C 350.4 179.4 359.0 170.3 367.5 176.1 C 376.0 181.8 384.6 203.7 393.1 211.3 C 401.7 218.9 410.2 215.3 418.8 221.4 C 427.3 227.5 435.8 239.9 444.4 248.0 C 452.9 256.1 465.7 266.3 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 267.6 77.1 260.8 85.6 255.6 C 94.2 250.4 102.7 243.3 111.2 238.9 C 119.8 234.4 128.3 233.3 136.9 229.1 C 145.4 225.0 154.0 218.4 162.5 214.0 C 171.0 209.5 179.6 205.4 188.1 202.6 C 196.7 199.8 205.2 197.3 213.8 196.9 C 222.3 196.5 230.8 201.8 239.4 200.1 C 247.9 198.4 256.5 187.8 265.0 186.6 C 273.5 185.5 282.1 191.9 290.6 193.5 C 299.2 195.0 307.7 195.4 316.2 196.1 C 324.8 196.7 333.3 193.7 341.9 197.1 C 350.4 200.5 359.0 211.7 367.5 216.4 C 376.0 221.1 384.6 222.0 393.1 225.4 C 401.7 228.8 410.2 232.1 418.8 236.9 C 427.3 241.7 435.8 248.7 444.4 254.2 C 452.9 259.7 465.7 267.4 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 268.5 77.1 264.7 85.6 261.2 C 94.2 257.7 102.7 251.8 111.2 249.0 C 119.8 246.2 128.3 245.0 136.9 244.3 C 145.4 243.5 154.0 244.9 162.5 244.4 C 171.0 244.0 179.6 245.2 188.1 241.5 C 196.7 237.7 205.2 224.0 213.8 221.8 C 222.3 219.6 230.8 226.6 239.4 228.1 C 247.9 229.6 256.5 229.7 265.0 230.7 C 273.5 231.8 282.1 235.3 290.6 234.3 C 299.2 233.3 307.7 225.4 316.2 224.6 C 324.8 223.8 333.3 226.2 341.9 229.7 C 350.4 233.2 359.0 242.6 367.5 245.5 C 376.0 248.4 384.6 246.6 393.1 247.2 C 401.7 247.8 410.2 246.8 418.8 249.2 C 427.3 251.5 435.8 258.1 444.4 261.6 C 452.9 265.1 465.7 268.6 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 269.9 77.1 268.9 85.6 269.6 C 94.2 270.3 102.7 274.9 111.2 274.3 C 119.8 273.6 128.3 267.5 136.9 265.5 C 145.4 263.6 154.0 262.0 162.5 262.7 C 171.0 263.3 179.6 269.1 188.1 269.3 C 196.7 269.5 205.2 265.8 213.8 263.8 C 222.3 261.9 230.8 257.2 239.4 257.6 C 247.9 258.1 256.5 263.0 265.0 266.4 C 273.5 269.9 282.1 275.7 290.6 278.1 C 299.2 280.5 307.7 283.6 316.2 280.7 C 324.8 277.7 333.3 263.8 341.9 260.4 C 350.4 256.9 359.0 258.5 367.5 259.9 C 376.0 261.4 384.6 267.4 393.1 268.8 C 401.7 270.3 410.2 268.2 418.8 268.8 C 427.3 269.3 435.8 272.0 444.4 272.2 C 452.9 272.4 465.7 270.4 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 271.7 77.1 276.6 85.6 279.9 C 94.2 283.3 102.7 287.0 111.2 290.2 C 119.8 293.5 128.3 297.7 136.9 299.5 C 145.4 301.3 154.0 299.4 162.5 301.2 C 171.0 303.1 179.6 310.2 188.1 310.6 C 196.7 311.0 205.2 303.3 213.8 303.6 C 222.3 304.0 230.8 310.9 239.4 312.8 C 247.9 314.7 256.5 314.1 265.0 315.2 C 273.5 316.3 282.1 321.1 290.6 319.3 C 299.2 317.5 307.7 307.1 316.2 304.5 C 324.8 301.8 333.3 304.1 341.9 303.5 C 350.4 302.9 359.0 303.0 367.5 300.9 C 376.0 298.7 384.6 293.3 393.1 290.6 C 401.7 288.0 410.2 286.8 418.8 285.0 C 427.3 283.1 435.8 282.0 444.4 279.5 C 452.9 277.0 465.7 271.6 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 272.4 77.1 278.7 85.6 284.4 C 94.2 290.2 102.7 299.6 111.2 304.6 C 119.8 309.5 128.3 308.9 136.9 314.3 C 145.4 319.8 154.0 332.8 162.5 337.2 C 171.0 341.6 179.6 338.3 188.1 340.6 C 196.7 343.0 205.2 348.0 213.8 351.2 C 222.3 354.5 230.8 358.4 239.4 360.2 C 247.9 362.0 256.5 362.6 265.0 362.1 C 273.5 361.6 282.1 361.0 290.6 357.2 C 299.2 353.4 307.7 341.7 316.2 339.2 C 324.8 336.6 333.3 342.2 341.9 341.9 C 350.4 341.5 359.0 342.2 367.5 337.1 C 376.0 331.9 384.6 317.1 393.1 310.8 C 401.7 304.4 410.2 303.1 418.8 299.0 C 427.3 294.8 435.8 290.5 444.4 285.7 C 452.9 280.9 465.7 272.6 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 274.1 77.1 286.9 85.6 294.4 C 94.2 302.0 102.7 307.9 111.2 315.6 C 119.8 323.2 128.3 331.6 136.9 340.2 C 145.4 348.8 154.0 362.0 162.5 367.0 C 171.0 372.0 179.6 367.1 188.1 370.2 C 196.7 373.4 205.2 383.8 213.8 385.6 C 222.3 387.5 230.8 379.6 239.4 381.3 C 247.9 383.1 256.5 393.1 265.0 396.0 C 273.5 398.9 282.1 398.9 290.6 398.6 C 299.2 398.4 307.7 397.7 316.2 394.4 C 324.8 391.0 333.3 385.5 341.9 378.6 C 350.4 371.7 359.0 358.2 367.5 352.9 C 376.0 347.6 384.6 351.7 393.1 346.7 C 401.7 341.7 410.2 331.7 418.8 323.0 C 427.3 314.3 435.8 303.3 444.4 294.4 C 452.9 285.6 465.7 274.1 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 275.3 77.1 291.5 85.6 301.9 C 94.2 312.2 102.7 323.0 111.2 332.1 C 119.8 341.3 128.3 347.5 136.9 356.9 C 145.4 366.4 154.0 380.0 162.5 388.8 C 171.0 397.6 179.6 403.8 188.1 409.8 C 196.7 415.8 205.2 420.1 213.8 424.7 C 222.3 429.3 230.8 435.8 239.4 437.5 C 247.9 439.3 256.5 435.4 265.0 435.2 C 273.5 435.1 282.1 437.3 290.6 436.5 C 299.2 435.8 307.7 435.4 316.2 430.6 C 324.8 425.7 333.3 414.6 341.9 407.3 C 350.4 400.0 359.0 394.7 367.5 386.7 C 376.0 378.6 384.6 367.6 393.1 359.2 C 401.7 350.7 410.2 345.5 418.8 336.0 C 427.3 326.5 435.8 313.3 444.4 302.3 C 452.9 291.3 465.7 275.4 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 277.0 77.1 299.1 85.6 312.3 C 94.2 325.5 102.7 337.4 111.2 349.1 C 119.8 360.7 128.3 370.8 136.9 382.2 C 145.4 393.7 154.0 406.9 162.5 417.5 C 171.0 428.2 179.6 439.7 188.1 445.9 C 196.7 452.1 205.2 450.1 213.8 454.6 C 222.3 459.2 230.8 471.2 239.4 473.1 C 247.9 475.0 256.5 464.7 265.0 465.9 C 273.5 467.0 282.1 480.9 290.6 480.1 C 299.2 479.4 307.7 468.2 316.2 461.5 C 324.8 454.8 333.3 446.9 341.9 440.2 C 350.4 433.4 359.0 431.1 367.5 420.9 C 376.0 410.7 384.6 391.3 393.1 379.0 C 401.7 366.7 410.2 359.0 418.8 347.2 C 427.3 335.4 435.8 321.0 444.4 308.1 C 452.9 295.2 465.7 276.3 470.0 270.0" fill="none" stroke="#e0524a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 60.0 270.0 C 64.3 270.9 77.1 271.5 85.6 275.1 C 94.2 278.7 102.7 283.9 111.2 291.4 C 119.8 299.0 128.3 313.8 136.9 320.6 C 145.4 327.3 154.0 329.0 162.5 331.9 C 171.0 334.7 179.6 332.1 188.1 337.7 C 196.7 343.2 205.2 361.8 213.8 365.4 C 222.3 369.0 230.8 365.8 239.4 359.3 C 247.9 352.7 256.5 338.9 265.0 326.0 C 273.5 313.1 282.1 294.9 290.6 282.0 C 299.2 269.2 307.7 257.2 316.2 249.0 C 324.8 240.8 333.3 233.5 341.9 232.9 C 350.4 232.3 359.0 240.6 367.5 245.3 C 376.0 249.9 384.6 256.3 393.1 260.8 C 401.7 265.2 410.2 270.2 418.8 272.0 C 427.3 273.8 435.8 272.0 444.4 271.7 C 452.9 271.4 465.7 270.3 470.0 270.0" fill="none" stroke="#56b947" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
  <path d="M 470.0 270.0 C 474.7 262.0 488.9 237.3 498.4 222.1 C 507.8 206.9 517.3 191.5 526.8 179.0 C 536.2 166.4 545.7 158.3 555.1 146.7 C 564.6 135.2 574.0 121.9 583.5 109.8 C 593.0 97.7 602.4 84.9 611.9 74.1 C 621.3 63.3 630.8 56.0 640.2 44.9 C 649.7 33.8 659.2 17.5 668.6 7.7 C 678.1 -2.1 687.5 -3.7 697.0 -14.0 C 706.5 -24.3 715.9 -43.0 725.4 -54.2 C 734.8 -65.3 744.3 -71.0 753.8 -80.9 C 763.2 -90.7 772.7 -103.0 782.1 -113.3 C 791.6 -123.6 801.0 -133.8 810.5 -142.5 C 820.0 -151.3 829.4 -158.0 838.9 -166.0 C 848.3 -173.9 857.8 -180.2 867.2 -190.0 C 876.7 -199.9 886.2 -215.4 895.6 -225.1 C 905.1 -234.8 919.3 -244.2 924.0 -248.0" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 262.4 488.9 238.7 498.4 224.7 C 507.8 210.7 517.3 198.0 526.8 186.1 C 536.2 174.2 545.7 163.5 555.1 153.5 C 564.6 143.4 574.0 137.0 583.5 126.0 C 593.0 115.1 602.4 99.1 611.9 87.8 C 621.3 76.5 630.8 68.2 640.2 58.4 C 649.7 48.6 659.2 38.1 668.6 29.2 C 678.1 20.3 687.5 14.9 697.0 5.1 C 706.5 -4.7 715.9 -20.8 725.4 -29.7 C 734.8 -38.6 744.3 -39.5 753.8 -48.2 C 763.2 -56.9 772.7 -73.3 782.1 -81.9 C 791.6 -90.6 801.0 -90.6 810.5 -100.0 C 820.0 -109.4 829.4 -128.2 838.9 -138.6 C 848.3 -149.0 857.8 -153.9 867.2 -162.4 C 876.7 -170.8 886.2 -181.1 895.6 -189.5 C 905.1 -197.9 919.3 -209.0 924.0 -212.9" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 263.2 488.9 241.2 498.4 229.2 C 507.8 217.2 517.3 208.3 526.8 198.1 C 536.2 188.0 545.7 179.2 555.1 168.4 C 564.6 157.6 574.0 142.8 583.5 133.4 C 593.0 124.0 602.4 120.1 611.9 111.9 C 621.3 103.8 630.8 94.9 640.2 84.5 C 649.7 74.2 659.2 59.7 668.6 49.6 C 678.1 39.4 687.5 31.3 697.0 23.5 C 706.5 15.7 715.9 10.4 725.4 2.8 C 734.8 -4.8 744.3 -14.4 753.8 -22.0 C 763.2 -29.5 772.7 -35.1 782.1 -42.6 C 791.6 -50.1 801.0 -58.4 810.5 -66.9 C 820.0 -75.4 829.4 -85.0 838.9 -93.7 C 848.3 -102.4 857.8 -111.4 867.2 -119.0 C 876.7 -126.6 886.2 -130.5 895.6 -139.2 C 905.1 -147.9 919.3 -165.8 924.0 -171.2" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 263.7 488.9 243.3 498.4 232.0 C 507.8 220.6 517.3 211.5 526.8 202.1 C 536.2 192.7 545.7 184.0 555.1 175.4 C 564.6 166.9 574.0 158.8 583.5 150.6 C 593.0 142.3 602.4 134.9 611.9 125.8 C 621.3 116.6 630.8 103.2 640.2 95.7 C 649.7 88.3 659.2 87.4 668.6 81.3 C 678.1 75.2 687.5 68.3 697.0 59.0 C 706.5 49.8 715.9 33.4 725.4 25.8 C 734.8 18.1 744.3 20.5 753.8 13.0 C 763.2 5.5 772.7 -11.6 782.1 -19.2 C 791.6 -26.8 801.0 -25.7 810.5 -32.5 C 820.0 -39.3 829.4 -52.4 838.9 -60.0 C 848.3 -67.6 857.8 -72.3 867.2 -78.0 C 876.7 -83.7 886.2 -88.4 895.6 -94.5 C 905.1 -100.5 919.3 -111.0 924.0 -114.4" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 264.5 488.9 247.4 498.4 237.3 C 507.8 227.2 517.3 218.4 526.8 209.4 C 536.2 200.4 545.7 190.3 555.1 183.2 C 564.6 176.1 574.0 173.2 583.5 166.8 C 593.0 160.3 602.4 152.9 611.9 144.5 C 621.3 136.1 630.8 123.4 640.2 116.2 C 649.7 109.0 659.2 107.9 668.6 101.1 C 678.1 94.3 687.5 83.0 697.0 75.4 C 706.5 67.9 715.9 61.9 725.4 56.0 C 734.8 50.1 744.3 47.6 753.8 40.0 C 763.2 32.4 772.7 16.6 782.1 10.6 C 791.6 4.6 801.0 10.0 810.5 4.1 C 820.0 -1.8 829.4 -17.6 838.9 -24.9 C 848.3 -32.2 857.8 -34.7 867.2 -39.9 C 876.7 -45.0 886.2 -50.0 895.6 -55.7 C 905.1 -61.5 919.3 -71.2 924.0 -74.3" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 265.1 488.9 249.4 498.4 240.7 C 507.8 232.0 517.3 225.1 526.8 218.0 C 536.2 210.9 545.7 205.5 555.1 198.2 C 564.6 190.9 574.0 180.6 583.5 174.1 C 593.0 167.6 602.4 165.5 611.9 159.2 C 621.3 152.8 630.8 143.3 640.2 136.2 C 649.7 129.1 659.2 123.0 668.6 116.3 C 678.1 109.7 687.5 103.1 697.0 96.4 C 706.5 89.6 715.9 81.0 725.4 75.7 C 734.8 70.4 744.3 68.8 753.8 64.8 C 763.2 60.8 772.7 58.9 782.1 51.7 C 791.6 44.6 801.0 28.7 810.5 21.9 C 820.0 15.1 829.4 15.4 838.9 11.0 C 848.3 6.5 857.8 1.5 867.2 -4.8 C 876.7 -11.0 886.2 -21.4 895.6 -26.7 C 905.1 -32.0 919.3 -35.0 924.0 -36.7" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 265.6 488.9 250.8 498.4 243.4 C 507.8 235.9 517.3 232.2 526.8 225.3 C 536.2 218.4 545.7 208.5 555.1 201.8 C 564.6 195.1 574.0 190.5 583.5 184.9 C 593.0 179.4 602.4 173.8 611.9 168.7 C 621.3 163.7 630.8 160.2 640.2 154.6 C 649.7 149.0 659.2 140.0 668.6 135.1 C 678.1 130.1 687.5 130.5 697.0 124.8 C 706.5 119.1 715.9 107.4 725.4 101.0 C 734.8 94.5 744.3 90.7 753.8 86.2 C 763.2 81.6 772.7 78.1 782.1 73.5 C 791.6 68.9 801.0 63.4 810.5 58.4 C 820.0 53.5 829.4 47.7 838.9 43.9 C 848.3 40.1 857.8 41.6 867.2 35.7 C 876.7 29.8 886.2 15.0 895.6 8.4 C 905.1 1.8 919.3 -2.0 924.0 -4.1" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 266.2 488.9 253.6 498.4 247.0 C 507.8 240.3 517.3 235.7 526.8 230.3 C 536.2 224.8 545.7 219.1 555.1 214.2 C 564.6 209.3 574.0 205.3 583.5 201.0 C 593.0 196.8 602.4 194.0 611.9 188.6 C 621.3 183.3 630.8 175.2 640.2 169.1 C 649.7 163.0 659.2 156.7 668.6 152.0 C 678.1 147.4 687.5 145.2 697.0 141.2 C 706.5 137.2 715.9 133.2 725.4 128.1 C 734.8 122.9 744.3 114.6 753.8 110.4 C 763.2 106.3 772.7 106.3 782.1 103.2 C 791.6 100.1 801.0 95.7 810.5 91.8 C 820.0 87.8 829.4 84.3 838.9 79.4 C 848.3 74.6 857.8 67.0 867.2 62.7 C 876.7 58.3 886.2 56.3 895.6 53.0 C 905.1 49.7 919.3 44.7 924.0 43.0" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 266.8 488.9 255.6 498.4 250.6 C 507.8 245.6 517.3 244.0 526.8 240.0 C 536.2 236.1 545.7 231.6 555.1 226.9 C 564.6 222.1 574.0 216.0 583.5 211.6 C 593.0 207.2 602.4 203.8 611.9 200.4 C 621.3 197.0 630.8 195.8 640.2 191.4 C 649.7 187.1 659.2 179.2 668.6 174.4 C 678.1 169.5 687.5 165.7 697.0 162.4 C 706.5 159.2 715.9 157.8 725.4 155.1 C 734.8 152.3 744.3 149.7 753.8 145.9 C 763.2 142.1 772.7 136.2 782.1 132.3 C 791.6 128.4 801.0 126.1 810.5 122.4 C 820.0 118.7 829.4 114.3 838.9 110.0 C 848.3 105.7 857.8 99.5 867.2 96.7 C 876.7 94.0 886.2 95.5 895.6 93.5 C 905.1 91.5 919.3 86.2 924.0 84.7" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 267.5 488.9 259.8 498.4 255.2 C 507.8 250.5 517.3 245.5 526.8 242.0 C 536.2 238.5 545.7 236.8 555.1 234.1 C 564.6 231.3 574.0 229.0 583.5 225.6 C 593.0 222.3 602.4 217.1 611.9 213.9 C 621.3 210.6 630.8 208.9 640.2 206.0 C 649.7 203.1 659.2 199.2 668.6 196.7 C 678.1 194.3 687.5 194.9 697.0 191.1 C 706.5 187.4 715.9 177.5 725.4 174.2 C 734.8 170.8 744.3 173.7 753.8 171.0 C 763.2 168.4 772.7 160.0 782.1 158.3 C 791.6 156.5 801.0 161.6 810.5 160.4 C 820.0 159.2 829.4 153.6 838.9 150.9 C 848.3 148.1 857.8 148.0 867.2 143.9 C 876.7 139.7 886.2 130.1 895.6 125.8 C 905.1 121.6 919.3 119.6 924.0 118.3" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 268.1 488.9 261.1 498.4 258.5 C 507.8 255.8 517.3 256.4 526.8 254.1 C 536.2 251.8 545.7 247.2 555.1 245.0 C 564.6 242.7 574.0 242.6 583.5 240.3 C 593.0 238.0 602.4 232.8 611.9 231.2 C 621.3 229.5 630.8 233.0 640.2 230.5 C 649.7 228.0 659.2 219.8 668.6 216.3 C 678.1 212.9 687.5 211.5 697.0 209.6 C 706.5 207.8 715.9 207.4 725.4 205.3 C 734.8 203.3 744.3 199.9 753.8 197.5 C 763.2 195.0 772.7 192.4 782.1 190.5 C 791.6 188.6 801.0 186.2 810.5 186.1 C 820.0 185.9 829.4 189.7 838.9 189.5 C 848.3 189.4 857.8 189.1 867.2 185.4 C 876.7 181.6 886.2 169.5 895.6 167.2 C 905.1 164.8 919.3 170.4 924.0 171.1" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 269.1 488.9 265.7 498.4 264.4 C 507.8 263.1 517.3 263.9 526.8 262.4 C 536.2 260.9 545.7 257.0 555.1 255.3 C 564.6 253.5 574.0 253.1 583.5 251.9 C 593.0 250.7 602.4 248.7 611.9 248.2 C 621.3 247.7 630.8 249.9 640.2 248.9 C 649.7 247.8 659.2 243.6 668.6 241.8 C 678.1 240.0 687.5 239.7 697.0 237.9 C 706.5 236.2 715.9 232.6 725.4 231.4 C 734.8 230.2 744.3 232.5 753.8 230.9 C 763.2 229.3 772.7 223.7 782.1 221.8 C 791.6 219.8 801.0 220.5 810.5 219.3 C 820.0 218.1 829.4 215.0 838.9 214.5 C 848.3 214.0 857.8 215.7 867.2 216.4 C 876.7 217.0 886.2 218.6 895.6 218.3 C 905.1 218.0 919.3 215.2 924.0 214.6" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <path d="M 470.0 270.0 C 474.7 269.9 488.9 270.4 498.4 269.5 C 507.8 268.7 517.3 265.1 526.8 264.9 C 536.2 264.6 545.7 267.4 555.1 267.9 C 564.6 268.4 574.0 268.2 583.5 267.8 C 593.0 267.4 602.4 265.5 611.9 265.6 C 621.3 265.6 630.8 268.3 640.2 268.1 C 649.7 267.9 659.2 266.0 668.6 264.4 C 678.1 262.8 687.5 259.1 697.0 258.5 C 706.5 257.9 715.9 261.0 725.4 261.0 C 734.8 261.0 744.3 258.8 753.8 258.5 C 763.2 258.2 772.7 259.0 782.1 259.0 C 791.6 259.1 801.0 259.6 810.5 258.8 C 820.0 258.1 829.4 255.6 838.9 254.5 C 848.3 253.3 857.8 252.3 867.2 251.7 C 876.7 251.1 886.2 250.0 895.6 250.7 C 905.1 251.4 919.3 255.0 924.0 255.8" fill="none" stroke="#56b947" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
  <line x1="474" y1="70" x2="474" y2="470" stroke="currentColor" stroke-width="6" opacity="0.45" stroke-linecap="round" />
  <text x="474" y="56" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="17" fill="currentColor" opacity="0.55" letter-spacing="2">TODAY</text>
  <circle cx="60" cy="270" r="7" fill="#56b947" />
  <circle cx="470" cy="270" r="7" fill="#56b947" />
  <text x="62" y="226" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="14" fill="currentColor" opacity="0.6" font-style="italic">you’re born</text>
  <line x1="474" y1="288" x2="474" y2="338" stroke="#56b947" stroke-width="1.5" opacity="0.7" />
  <text x="474" y="356" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="14" fill="#56b947" font-style="italic">your life, today</text>
  <line x1="170" y1="512" x2="430" y2="512" stroke="currentColor" stroke-width="2" opacity="0.5" stroke-linecap="round" />
  <polyline points="180,506 168,512 180,518" fill="none" stroke="currentColor" stroke-width="2" opacity="0.5" stroke-linecap="round" stroke-linejoin="round" />
  <text x="300" y="540" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="15" fill="currentColor" opacity="0.6" letter-spacing="3">THE PAST</text>
  <line x1="518" y1="512" x2="800" y2="512" stroke="currentColor" stroke-width="2" opacity="0.5" stroke-linecap="round" />
  <polyline points="790,506 802,512 790,518" fill="none" stroke="currentColor" stroke-width="2" opacity="0.5" stroke-linecap="round" stroke-linejoin="round" />
  <text x="660" y="540" text-anchor="middle" font-family="Source Serif 4, Georgia, serif" font-size="15" fill="currentColor" opacity="0.6" letter-spacing="3">THE FUTURE</text>
  <rect x="60" y="78" width="20" height="14" fill="#e0524a" />
  <text x="90" y="90" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="currentColor" opacity="0.7">paths closed to you</text>
  <rect x="60" y="102" width="20" height="14" fill="#56b947" />
  <text x="90" y="114" font-family="Source Serif 4, Georgia, serif" font-size="13" fill="currentColor" opacity="0.7">paths open to you</text>
</svg>
<figcaption>Life paths, after Tim Urban’s diagram for Wait But Why. Red: closed to you. Green: still open.</figcaption>
</figure>

<h2 id="conclusion">Conclusion</h2>

<p>I don’t know if these three metrics are right. I don’t think right is the point. They’re the ones I can’t stop watching.</p>

<p>I still play table tennis on office breaks and weekends. I still feel the old reflex twitch when someone pulls ahead, the imaginary scoreboard lighting up. But more often now I let the rally run long. I feed an easy one back, try a stupid loop just to see if it lands, lose the point on purpose, because a rally that ends early isn’t worth winning. That’s the one metric I trust most. Not the rate, not the problems, not the options. Just this: I can finally lose on purpose. A younger me would have called that losing. He’d have been wrong.</p>

<p>Maybe one day I’ll get tired of exploring and want to stay still. I used to think staying still was the failure. Now I think wanting to stay still might be the biggest change of all. When that day comes, that’ll be growth too.</p>]]></content><author><name>Parth Batra</name></author><category term="note-to-self" /><category term="brain-glitch" /><summary type="html"><![CDATA[During table tennis practice last year, I caught myself counting imaginary scores. Not the actual rally. The implied one. This person broke it five times, I broke it three, therefore I’m losing. At practice. Against friends. No stakes whatsoever. I’d done it my whole life: hackathons, group projects, anything with a scoreboard real or invented. If I was in, I was in to win.]]></summary></entry><entry><title type="html">Spiti: Five Days, One Mountain, One Arm-Wrestle I’m Still Losing.</title><link href="https://fromparth.blog/2026/06/08/spiti-took-five-days-to-take-me-apart.html" rel="alternate" type="text/html" title="Spiti: Five Days, One Mountain, One Arm-Wrestle I’m Still Losing." /><published>2026-06-08T00:00:00+00:00</published><updated>2026-06-08T00:00:00+00:00</updated><id>https://fromparth.blog/2026/06/08/spiti-took-five-days-to-take-me-apart</id><content type="html" xml:base="https://fromparth.blog/2026/06/08/spiti-took-five-days-to-take-me-apart.html"><![CDATA[<style>
.video-wrapper {
  margin: 1.25rem 0;
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
}
.video-wrapper video {
  width: 100%;
  height: auto;
  display: block;
  max-height: 80vh;
}
</style>

<p>It rained the morning we left Spiti.</p>

<figure class="diagram">
<svg viewBox="0 0 380 150" width="100%" height="auto" role="img" aria-label="Small doodles of things from the Spiti trip: rain, prayer flags, the Milky Way, a mountain peak, an arm-wrestle, a birthday cake, a bonfire, a river, and the last sunset." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g transform="translate(75,38)" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
    <path d="M-9 -4 a5 5 0 0 1 9 -3 a4 4 0 0 1 5 4" />
    <line x1="-5" y1="3" x2="-7" y2="9" />
    <line x1="1" y1="3" x2="-1" y2="9" />
    <line x1="7" y1="3" x2="5" y2="9" />
  </g>
  <g transform="translate(190,38)" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round">
    <path d="M-13 -6 Q0 -11 13 -6" />
    <path d="M-7 -6 L-3 -6 L-5 1 Z" fill="currentColor" />
    <path d="M3 -6 L7 -6 L5 1 Z" fill="currentColor" />
  </g>
  <g transform="translate(305,38)" fill="currentColor">
    <path d="M0 -9 L2.5 -2.5 L9 0 L2.5 2.5 L0 9 L-2.5 2.5 L-9 0 L-2.5 -2.5 Z" />
    <circle cx="-11" cy="-7" r="1.5" />
    <circle cx="11" cy="6" r="1.5" />
  </g>
  <g transform="translate(75,75)" stroke="currentColor" stroke-width="2" fill="none" stroke-linejoin="round">
    <path d="M-13 8 L-3 -8 L4 1 L13 8 Z" />
    <path d="M-6 -3 L-3 -8 L0 -3 Z" fill="currentColor" />
  </g>
  <g transform="translate(190,75)" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round">
    <line x1="-13" y1="9" x2="-5" y2="0" />
    <line x1="13" y1="9" x2="5" y2="0" />
    <circle cx="-4" cy="-2" r="4.5" fill="currentColor" stroke="none" />
    <circle cx="4" cy="-2" r="4.5" fill="currentColor" stroke="none" />
  </g>
  <g transform="translate(305,75)" stroke="currentColor" stroke-width="2" fill="none" stroke-linejoin="round">
    <rect x="-9" y="-2" width="18" height="10" rx="1.5" />
    <line x1="0" y1="-2" x2="0" y2="-8" stroke-linecap="round" />
    <circle cx="0" cy="-10" r="2.2" fill="currentColor" stroke="none" />
  </g>
  <g transform="translate(75,112)">
    <path d="M0 8 C -8 3 -5 -3 -2 -8 C 0 -4 1 -3 0 -8 C 3 -4 8 1 0 8 Z" fill="currentColor" />
    <line x1="-10" y1="8" x2="10" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
  </g>
  <g transform="translate(190,112)" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round">
    <path d="M-12 -4 Q-6 -7 0 -4 T12 -4" />
    <path d="M-12 2 Q-6 -1 0 2 T12 2" />
    <path d="M-12 8 Q-6 5 0 8 T12 8" />
  </g>
  <g transform="translate(305,112)" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round">
    <path d="M-12 2 A10 10 0 0 1 12 2" />
    <line x1="-13" y1="2" x2="13" y2="2" />
    <line x1="-7" y1="8" x2="7" y2="8" opacity="0.5" />
  </g>
</svg>
<figcaption>Five days up and down the valley. It took all five to take me apart.</figcaption>
</figure>

<p>Six days of hard blue skies, the kind that make you squint at your own photos later, and the valley picked our last day to rain. We stood next to our bus, bags fully loaded, nobody moving towards the door. Just standing there, feeling the drizzle, like the rain was saying something and it would be rude to leave mid-sentence.</p>

<p>Mountains don’t wave goodbye. They just rain.</p>

<p>But let me go back.
<img src="/assets/images/spiti-assets/spiti-post-banner.jpg" alt="Spiti valley road winding through the mountains" /></p>

<hr />

<h2 id="day-one-the-point-where-maps-become-decorative">Day One: The Point Where Maps Become Decorative</h2>

<p>The road from Shimla into Kinnaur is carved into the side of a cliff. Not metaphorically. Someone took a drill to a rock face several hundred meters above the Sutlej and decided that was a road now, and for sixty years everyone has agreed to believe him.</p>

<p>Our driver, a man of monk-grade calm, took these roads the way I drink coffee. <em>Automatic. Present. Unbothered</em>.</p>

<p>The rest of us had a more complicated relationship with the route. I won’t say we were scared. I’ll say we developed a sudden, collective interest in the view from the left side of the car specifically, and that someone turned on WhatsApp live location for a family group chat. I don’t know what we thought Gurgaon was going to do about a cliff in Kinnaur. It just felt important that they watch.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/driver-ko-yakeen-tha.mp4" type="video/mp4" />
</video>
</div>

<p>At one point the side mirror cleared the rock face by a margin we engineers call “acceptable”, but more commonly “bhaiya, itni side se mat lo!”</p>

<p>By Kalpa I had made peace with the roads. Convenient, because Spiti had five more days of them.</p>

<hr />

<h2 id="kalpa-the-flags-pray-without-you">Kalpa: The Flags Pray Without You</h2>

<p>Kalpa is where the trip stopped being a road trip and started being whatever it ended up becoming.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/rekong-peo-prayer-flags-mountains.mp4" type="video/mp4" />
</video>
</div>

<p>The prayer flags here don’t flutter, they spiral. Wind comes up the valley and the flags move like the landscape choreographed them. Each one is a prayer released into the air. I stood there trying to compute how many prayers had passed through that exact patch of sky over a few centuries, and concluded the math was not the point.</p>

<p>Kinnaur Kailash sat across the valley the whole evening, lit up like it knew. My camera captured it. My brain accused the camera of editing. The camera was innocent. The place just looks like that.</p>

<p><img src="/assets/images/spiti-assets/kalpa-drinking-tea-mountain-bg.jpg" alt="Drinking tea with the mountains as a backdrop" /></p>

<hr />

<h2 id="tabo-nobody-tells-you-about-the-sky-thats-on-purpose">Tabo: Nobody Tells You About the Sky. That’s On Purpose.</h2>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/har-jhanda-ek-dua-hai.MP4" type="video/mp4" />
</video>
</div>

<p>The caves above Tabo are a twenty-minute trek from the village, up a trail that is marked the way secrets are marked, which is to say not at all. Monks used them for meditation for a thousand years. I climbed up at night with a lot of optimism in my phone’s camera capabilities.</p>

<p>Here’s what happened: I set up a twenty-minute timelapse of the sky, sat down on the cold ground, looked up, and didn’t say a word until the camera was done. Neither did anyone else. The starry night over Tabo isn’t a sight, it’s a ceiling. You think you’re prepared because you’ve seen photos and you consider yourself a person who appreciates a sky. You’re not prepared. It hangs there with absolutely no regard for what it’s doing to you. No camera can ever do justice to that!</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/tabo-caves-ke-upar-ka-aasmaan.mp4" type="video/mp4" />
</video>
</div>

<p>The monastery below has murals over a thousand years old. The sky above has a few billion years on that. Both were watching us. Only one of us blinked.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/tabo-monastry.MP4" type="video/mp4" />
</video>
</div>

<hr />

<h2 id="kaza-four-seconds">Kaza: Four Seconds</h2>

<p><img src="/assets/images/spiti-assets/kaza-background-1.jpg" alt="Kaza valley landscape" /></p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/kaza-city-timelapse.mp4" type="video/mp4" />
</video>
</div>

<p>I should be honest about what I thought I was doing in Spiti.</p>

<p>I thought I was the guy who does pushups at what the signboard insists is the highest motorable road in the world. Who jumps in slow motion at the highest café in the world. Who shows up at 4,500 meters and meets the altitude on his own terms. And to be fair, all of that happened.</p>

<p>The pushups happened at the top of the pass, in air thin enough that my lungs filed a formal complaint by rep six. I finished the set anyway. There’s something about effort at that altitude that strips the pose out of it. You’re not performing, your body is too busy negotiating with the oxygen for you to perform. It’s just you, the ground, and mountains watching with complete indifference.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/sea-level-is-for-the-weak.mp4" type="video/mp4" />
</video>
</div>

<p>The slo-mo jump at the highest café in the world was filmed, reviewed, and refilmed, and looked significantly more cinematic in my head than on camera. Gravity at 15,000 feet works exactly like gravity at sea level. I have no idea why I expected a discount!</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/gravity%20nei%20koshish%20ki.mp4" type="video/mp4" />
</video>
</div>

<p>Then, at our homestay in Kaza, the owner’s daughter who could not have been more than seven, challenged me to arm wrestling.</p>

<p>She had the energy of someone who had done this before. I realized mid-lock that she absolutely had, that I was not her first tourist challenger, and that the outcome had been decided before I sat down. Four seconds. No trash talk, no celebration. She went back to her food the way you move on from something that was never really a question, while I sat there recalibrating my entire understanding of the week.</p>

<p>Five days of altitude couldn’t humble me, because I was too busy doing pushups at it.</p>

<p>A seven year old needed four seconds.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/haar-gya-dil-se.mp4" type="video/mp4" />
</video>
</div>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/world-highest-postoffice.mp4" type="video/mp4" />
</video>
</div>
<hr />

<h2 id="komic-the-sacred-absurd">Komic: The Sacred Absurd</h2>

<p>Komic claims the title of highest village in Asia connected by motorable road, sits at more than 15,000 feet, and contains one monastery, a handful of houses, and on the day we visited, six adults doing Chaiya Chaiya in front of the Buddha statue a few hundred metres from the top.</p>

<p>I can’t fully reconstruct how it started. Someone hummed. The hum found a second voice. And then it wasn’t humming anymore, it was the full routine, train-roof energy, shoulders and all, in front of highest buddha statue in the world.</p>

<p>Buddha didn’t object. I choose to believe he was deep in meditation. I also believe, with equal conviction, that he has seen worse.</p>

<p>There’s a version of this story where it reads as disrespect. I don’t think it was. I think when a place overwhelms you past a certain point, you respond with the most instinctive joy you own. Ours happened to be a 90s Shah Rukh number. The Buddha kept smiling. Statues do that, but still.</p>

<p><img src="/assets/images/spiti-assets/komic-mei-chaiya-chaiya.jpg" alt="Chaiya Chaiya in front of the Komic monastery" /></p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/worlds-highest-village.mp4" type="video/mp4" />
</video>
</div>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/komic-mei-chaiya-chaiya-video.mp4" type="video/mp4" />
</video>
</div>

<hr />

<h2 id="kalpa-again-27-at-9000-feet">Kalpa Again: 27 at 9,000 Feet</h2>

<p>Coming back through a place you’ve already seen has a different texture. Kalpa on the way in was a view. Kalpa on the way back was a verdict, the same valley asking what you did with the last five days.</p>

<p>The cake came out as soon as the clock hit 12.</p>

<p>The cake came out because someone in the room had decided the clock wouldn’t hit twelve without it. She’d found the one bakery on the way to Kalpa that half exists, convinced it to fully exist for a day, and made sure I spent the first minute of twenty-seven laughing instead of just being tired. The cake tasted like that effort — incredible, and a little structurally unsound.</p>

<p><img src="/assets/images/spiti-assets/27th-birthday-1.JPG" alt="Birthday cake at 9,000 feet in Kalpa" /></p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/27th-birthday-2.MP4" type="video/mp4" />
</video>
</div>

<p>The bonfire that night turned everything orange. Cold came down from the valley, the fire pushed back, and we sat inside that negotiation for hours. Around midnight I caught myself thinking: I don’t want more Spiti, exactly. I want more of this. The thing where you’re somewhere you chose, with people you chose, and there is nowhere else you’re supposed to be.</p>

<hr />

<h2 id="chitkul-the-river-knows">Chitkul: The River Knows</h2>

<p>Chitkul is the last village before the Indo-Tibetan border. Past it you need permits, and past those you need a different passport. The Baspa river runs through it carrying glacier melt that is the exact colour of somewhere you haven’t been.</p>

<p>I did pushups on the bank. Mid-rep, my face passed close enough to the water that an opportunity presented itself, and I have very little self-control near rivers. So I dipped my face in. Snowmelt cold is not regular cold. Regular cold is a tap on the shoulder. This was a slap with paperwork. I came up instantly, reconsidered my life, and went back down for another.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/chitkul-river-1.mp4" type="video/mp4" />
</video>
</div>

<p>Later I found a flat rock by the water and lay down on it, intending to rest for a moment. The river was exactly the right sound and the rock was exactly the right temperature, and then it was twenty minutes later and someone was taking photos of me for evidence.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/chitkul-river-2.mp4" type="video/mp4" />
</video>
</div>

<hr />

<h2 id="the-last-sunset-and-then-the-rain">The Last Sunset, and Then the Rain</h2>

<p>The sunset on the last evening was too good. I mean that as an accusation. It felt like the mountains had checked the calendar, seen we were leaving in the morning, and decided to run up the score. The sky went through its colours in the wrong order. The valley filled with light at angles that don’t survive geometry. We took photos knowing the photos would fail, because you have to try anyway.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/last-day-sunset.mp4" type="video/mp4" />
</video>
</div>

<p>And in the morning, it rained.</p>

<p>Not a storm. Nothing dramatic. Just steady, considered drizzle, falling on roads we were about to drive out on, after five days of skies so clear they felt personal.</p>

<p>So there we were. Bags half-loaded. Nobody moving towards the bus.</p>

<p>I still don’t know if it was the valley saying goodbye or saying <em>you left something here</em>. I’ve turned it over a few times since. Both feel true, and neither explains why none of us could get in the bus.</p>

<p>We drove out of Spiti in the rain. I kept looking back, at the flags, at the roads, at all the ego I’d left at altitude, at a homestay in Kaza where a seven-year-old owns four seconds of my life. We didn’t talk much on the drive down. Not because we were tired.</p>

<p>Some things you don’t want to name yet.</p>

<div class="video-wrapper">
<video autoplay="" muted="" loop="" playsinline="" preload="metadata">
  <source src="/assets/images/spiti-assets/last-day-rain.mp4" type="video/mp4" />
</video>
</div>

<hr />

<script>
(function() {
  const videos = document.querySelectorAll('.video-wrapper video');
  if (!videos.length) return;

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      const video = entry.target;
      if (entry.isIntersecting) {
        video.play().catch(() => {});
      } else {
        video.pause();
      }
    });
  }, { threshold: 0.3 });

  videos.forEach(video => observer.observe(video));
})();
</script>

<p><img src="/assets/images/spiti-assets/kee-monastry.JPG" alt="Kee Monastery nestled in the Spiti valley" />
<em>Spiti doesn’t let you leave with all your edges intact. That’s probably the point.</em></p>]]></content><author><name>Parth Batra</name></author><category term="nomading" /><category term="note-to-self" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">I Built a Voice AI in 30 Days. Its Hardest Bug Took Three.</title><link href="https://fromparth.blog/2026/05/24/building-an-open-source-vapi-alternative.html" rel="alternate" type="text/html" title="I Built a Voice AI in 30 Days. Its Hardest Bug Took Three." /><published>2026-05-24T00:00:00+00:00</published><updated>2026-05-24T00:00:00+00:00</updated><id>https://fromparth.blog/2026/05/24/building-an-open-source-vapi-alternative</id><content type="html" xml:base="https://fromparth.blog/2026/05/24/building-an-open-source-vapi-alternative.html"><![CDATA[<p>You call a number, an AI picks up, you say “hello,” and it says “hello” back. Feels instant. Feels simple. It is neither.</p>

<figure class="diagram">
<svg viewBox="0 0 380 150" width="100%" height="auto" role="img" aria-label="A filmstrip unspooling one second into five frames: phone handset, globe network, microphone, sound waves becoming text, and a speech bubble." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <!-- Sprocket holes top -->
  <g fill="none" stroke="currentColor" stroke-width="2" opacity="0.5">
    <rect x="48" y="4" width="12" height="8" rx="2" />
    <rect x="106" y="4" width="12" height="8" rx="2" />
    <rect x="164" y="4" width="12" height="8" rx="2" />
    <rect x="222" y="4" width="12" height="8" rx="2" />
    <rect x="280" y="4" width="12" height="8" rx="2" />
    <rect x="338" y="4" width="12" height="8" rx="2" />
  </g>
  <!-- Sprocket holes bottom -->
  <g fill="none" stroke="currentColor" stroke-width="2" opacity="0.5">
    <rect x="48" y="138" width="12" height="8" rx="2" />
    <rect x="106" y="138" width="12" height="8" rx="2" />
    <rect x="164" y="138" width="12" height="8" rx="2" />
    <rect x="222" y="138" width="12" height="8" rx="2" />
    <rect x="280" y="138" width="12" height="8" rx="2" />
    <rect x="338" y="138" width="12" height="8" rx="2" />
  </g>
  <!-- Filmstrip edge lines -->
  <line x1="20" y1="16" x2="360" y2="16" stroke="currentColor" stroke-width="2" />
  <line x1="20" y1="134" x2="360" y2="134" stroke="currentColor" stroke-width="2" />
  <!-- Vertical frame dividers -->
  <g fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.3">
    <line x1="80" y1="16" x2="80" y2="134" />
    <line x1="140" y1="16" x2="140" y2="134" />
    <line x1="200" y1="16" x2="200" y2="134" />
    <line x1="260" y1="16" x2="260" y2="134" />
    <line x1="320" y1="16" x2="320" y2="134" />
  </g>
  <!-- Frame 1: Phone handset -->
  <g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M44 50 c-2 8 -2 18 0 26 l3 0 c0 -8 0 -18 0 -26 z" />
    <path d="M56 50 c2 8 2 18 0 26 l-3 0 c0 -8 0 -18 0 -26 z" />
    <rect x="44" y="56" width="12" height="14" rx="2" />
  </g>
  <!-- Frame 2: Globe/network -->
  <g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <circle cx="110" cy="75" r="12" />
    <ellipse cx="110" cy="75" rx="6" ry="12" />
    <line x1="98" y1="69" x2="122" y2="69" />
    <line x1="98" y1="81" x2="122" y2="81" />
  </g>
  <!-- Frame 3: Microphone -->
  <g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <rect x="165" y="62" width="10" height="16" rx="5" />
    <path d="M162 72 c0 8 8 12 16 12" />
    <line x1="170" y1="84" x2="170" y2="90" />
    <line x1="164" y1="90" x2="176" y2="90" />
  </g>
  <!-- Frame 4: Sound-wave to text -->
  <g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M222 69 q2 -4 4 0 q2 4 4 0 q2 -4 4 0" />
    <path d="M223 77 q1 -3 2 0 q1 3 2 0 q1 -3 2 0" />
    <line x1="240" y1="64" x2="254" y2="64" />
    <line x1="240" y1="72" x2="252" y2="72" />
    <line x1="240" y1="80" x2="248" y2="80" />
    <line x1="240" y1="88" x2="254" y2="88" />
  </g>
  <!-- Frame 5: Speech bubble -->
  <g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M282 62 h26 c4 0 6 3 6 6 v12 c0 4 -2 6 -6 6 h-14 l-6 5 v-5 h-6 c-4 0 -6 -2 -6 -6 v-12 c0 -3 2 -6 6 -6 z" />
    <line x1="288" y1="70" x2="306" y2="70" stroke-width="1.5" />
    <line x1="288" y1="76" x2="300" y2="76" stroke-width="1.5" />
  </g>
  <!-- "1 sec" label spanning the strip -->
  <text x="190" y="130" text-anchor="middle" fill="currentColor" font-size="13" font-weight="600" opacity="0.6">1 sec</text>
</svg>
<figcaption>Everything that has to fire in the second after you say hello.</figcaption>
</figure>

<p>In the second after you speak, your voice leaves your phone, crosses a telephony network that still speaks a protocol older than the web, lands on a server that has to decide you’re actually talking and not just breathing, gets transcribed into text, gets sent to a large language model that thinks up a reply, gets turned back into a human-sounding voice, and gets pushed back down the same line to your ear. If any one of those steps takes too long, the AI talks over you, or sits in dead silence, or answers a question you didn’t ask. All of it has to happen in under a second, every turn, on every call, forever.</p>

<p>Here’s the part nobody tells you: the “AI” in that chain is the easy 10%. The model is a single API call. The other 90% is plumbing. It’s the WebSocket that times out silently at 3am. It’s a recording bug where the bot’s audio packs itself tight with no silence between turns, so the saved call sounds like the AI is talking over itself. It’s an LLM that leaks <code class="language-plaintext highlighter-rouge">&lt;function&gt;call_api()&lt;/function&gt;</code> into its output, and the voice engine reads it aloud as “less than function call api greater than.” It’s a race condition that drops the very first thing the AI says on every other call.</p>

<p>I spent a month building openVAPI, an open-source, self-hosted alternative to Vapi and Retell. About 30 days. Around 4,600 lines of core pipeline code. One very long 3:35 AM commit. And I wrote every line of it.</p>

<p>This is the engineering journal. The wins, the disasters, and what the git log actually says.</p>

<h3 id="the-anatomy-of-that-second">The Anatomy of That Second</h3>

<p>Before the war stories, here’s where the second actually goes. These aren’t marketing numbers. The first row is a value I chose myself, the rest are typical figures for the providers in my stack, and later you’ll meet the <code class="language-plaintext highlighter-rouge">TurnMetrics</code> dataclass that stamps every one of these stages on every real turn.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Stage</th>
      <th style="text-align: left">Typical time</th>
      <th style="text-align: left">Where the number comes from</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Confirming you’ve actually stopped talking</td>
      <td style="text-align: left">800 ms</td>
      <td style="text-align: left">Configured: Silero VAD <code class="language-plaintext highlighter-rouge">stop_secs=0.8</code></td>
    </tr>
    <tr>
      <td style="text-align: left">Final transcript from STT</td>
      <td style="text-align: left">100-300 ms</td>
      <td style="text-align: left">Typical for Deepgram streaming</td>
    </tr>
    <tr>
      <td style="text-align: left">First token from the LLM</td>
      <td style="text-align: left">200-400 ms</td>
      <td style="text-align: left">Typical for Groq and OpenAI</td>
    </tr>
    <tr>
      <td style="text-align: left">First audio byte from TTS</td>
      <td style="text-align: left">75-150 ms</td>
      <td style="text-align: left">Typical for ElevenLabs Flash</td>
    </tr>
    <tr>
      <td style="text-align: left">Audio back down the line</td>
      <td style="text-align: left">50-100 ms</td>
      <td style="text-align: left">20 ms chunks plus the carrier network</td>
    </tr>
  </tbody>
</table>

<p>Add it up and the full path from your last syllable runs somewhere between 1.2 and 1.7 seconds. The sub-second figure everyone quotes starts at row two, after the silence check. Which is fair, because that 800 ms is not the AI being slow. It’s the pipeline being polite.</p>

<p>Cut the silence window shorter and the bot interrupts you every time you pause to think. Leave it longer and every reply feels like a long-distance call from 1991. The single biggest lever in voice AI latency has nothing to do with the AI. It’s deciding how long a human silence has to last before it counts as a turn.</p>

<h3 id="the-setup">The Setup</h3>

<p>On May 11, 2026, I committed the entire scaffold in one shot: a FastAPI backend (Python’s async web framework) with CRUD for assistants, phone numbers, and call logs. A Next.js UI with a dark theme, because I already knew what hours I’d be keeping. Postgres for durable storage, Redis for fast in-memory state, Docker to run it all. 57 files, 3,261 lines. The architecture was deliberate from day one: one process, no worker pool, no threads. Every incoming call rides on an asyncio Task. All IO is awaited. A semaphore caps concurrent calls at 50. You scale by running more pods.</p>

<p>Three days later, the voice pipeline landed. I built it on top of <strong>Pipecat</strong>, a Python framework for real-time voice AI from the folks at Daily. Pipecat handles the hard distributed-systems problems: moving audio around, passing it between processing stages as “frames,” and managing whose turn it is to speak. What I built on top was the production layer Pipecat doesn’t give you.</p>

<p>That layer is a tour of the modern voice stack. <strong>Twilio and Plivo</strong> for telephony (the actual phone carriers that own the numbers and ring the call). <strong>Deepgram</strong> for STT, speech-to-text, turning what the caller says into words on the wire. <strong>ElevenLabs</strong> for TTS, text-to-speech, turning the AI’s reply back into a voice that sounds human. <strong>OpenAI and Groq</strong> as the LLM, the brain that decides what to say. I wired them together with a plugin registry for the carriers, a service factory, a pipeline builder, event handlers with per-turn millisecond timing, and cost calculation in a <code class="language-plaintext highlighter-rouge">pricing.toml</code>. Four vendors, four billing models, one TOML file to keep them all honest.</p>

<p>One detail that trips up everyone: phone audio is bad on purpose. The telephony pipeline runs at 8kHz mulaw, a low-fidelity, narrow-band format designed in an era when bandwidth was precious. It’s why every phone call sounds slightly muffled. That commit was 69 files, 14,521 lines. The first phone call actually connected.</p>

<p>Around May 18, I added browser calls. For those I used <strong>WebRTC</strong>, the same peer-to-peer audio and video tech that powers Google Meet and Discord. Getting a server to speak WebRTC (browsers do it natively, servers don’t) means <strong>aiortc</strong>, the Python library that implements the protocol, wrapped here by Pipecat’s <code class="language-plaintext highlighter-rouge">SmallWebRTCTransport</code>. Browser audio comes in at a cleaner 16kHz instead of the phone’s 8kHz, so the same pipeline now has to juggle two sample rates depending on where the call came from. I also built a cost calculator so I’d know exactly what each call cost, down to the token and the character. On the cheapest stack (Groq for the brain, Deepgram for the ears, ElevenLabs for the voice, Twilio for the line), a 3-minute call comes out to roughly $0.10. About 8 rupees. Less than the chai you’d drink while debugging it.</p>

<h3 id="1-am-docker-compose-up-again">1 AM, <code class="language-plaintext highlighter-rouge">docker compose up</code>, Again</h3>

<p>The git log for May 20 is telling. Between 01:13 and 02:04, there are 28 commits, all in under an hour. What was I actually doing? Trying to get a single outbound phone call to connect end to end.</p>

<p>Telephony providers do not agree on anything. The handshake itself is a little dance: the carrier answers the call, pings your server over the internet, you hand back a set of instructions that tells it to open a live audio socket, and then it streams the call’s raw audio to you in real time. Twilio sends that audio as JSON with base64-encoded mulaw. Plivo uses a slightly different framing. A serializer has to abstract both so the rest of the pipeline just sees a uniform <code class="language-plaintext highlighter-rouge">InputAudioRawFrame</code> and <code class="language-plaintext highlighter-rouge">OutputAudioRawFrame</code> and never has to care which carrier is on the line. Getting that right took 14 attempts in 51 minutes.</p>

<p>That’s what those 28 commits are: each attempt left a pair of snapshots. Not a clean, beautiful git history. One person at 1 AM, running <code class="language-plaintext highlighter-rouge">docker compose up</code> again and again, reading logs, fixing one framing issue at a time. Every run started with the same thought, <em>this is the one that fixes it</em>, and run 14 finally was.</p>

<h3 id="wholesale-improvements">“wholesale improvements”</h3>

<p>On May 27, I made my biggest single commit and titled it “wholesale improvements” because I genuinely couldn’t think of a better name. 47 files, 1,913 insertions, 312 deletions. Here’s what was in it:</p>

<p><strong>The webhook service.</strong> When a call ends, you often need to tell some other server about it. That’s a webhook, an HTTP call your system makes outward. Mine signs every payload with HMAC-SHA256 (so the receiver can prove it really came from me) and retries on failure with exponential backoff at [0, 5, 30, 300, 1800] seconds. The last retry fires half an hour later, which in webhook time is a formal apology letter. A reconciliation loop finds unsent webhooks on startup and fires them. Every attempt is logged with status code, response body, and error.</p>

<p><strong>The Vapi compatibility layer.</strong> <code class="language-plaintext highlighter-rouge">vapi_compat.py</code> exposes a REST shape that matches Vapi’s API exactly. Any tool already built against Vapi can point at openVAPI by changing one URL. That single decision probably removed more adoption friction than any feature I could build.</p>

<p><strong>The background scheduler with crash recovery.</strong> An asyncio task that wakes every 60 seconds, dials any call that was queued for a future time, and cleans up orphans: if the server crashed mid-call, on restart it finds every call stuck in limbo, hangs it up through the carrier’s API, flushes the half-finished transcript from Redis to Postgres, and returns the phone number to the pool.</p>

<p><strong>S3 storage, consolidated database migrations, and rewritten event handlers</strong> with a <code class="language-plaintext highlighter-rouge">TurnMetrics</code> dataclass that times every stage of every turn: when speech was detected, when the transcript finalized, when the LLM started, when its first token arrived, when it finished, when the voice started speaking, and when it stopped. Every turn, every call, in milliseconds.</p>

<p>That’s the thing about voice AI. The intelligence is an API call. The work is everything around it.</p>

<h3 id="the-disaster-section">The Disaster Section</h3>

<p>Four bugs. Each one cost me days. Each one taught me something no tutorial covers.</p>

<p><strong>Bug 1: The recording desync (3 days)</strong></p>

<p>This was the hardest bug in the whole project. It took three days just to understand. Once the math finally clicked, the fix itself landed in a 40-minute burst of commits on the evening of May 28.</p>

<p>The symptom: saved call recordings were broken. The system records both sides of a call, the human and the AI, then mixes them into one audio file. But the bot’s audio was packed tight with no silence between its turns. When the mixer combined the two tracks, all of the bot’s turns landed at the very start. The recording sounded like [bot1, bot2, bot3… then user1… user2], two people in a conversation where one of them lives in the past.</p>

<p>The root cause was subtle. The processor capturing bot audio stored raw bytes with no timestamps. The mixer had no idea <em>when</em> each burst of speech actually happened, so it just concatenated them. The human’s audio, meanwhile, spanned the real call timeline correctly, because its capture processor sat earlier in the pipeline and saw every frame. The bot’s audio was positionless.</p>

<p>The fix: store each chunk as a <code class="language-plaintext highlighter-rouge">(wall_clock_offset, audio_bytes)</code> pair, then rebuild the full track by dropping each burst at its real-time position, with silence filling the gaps between.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_build_bot_track</span><span class="p">(</span><span class="n">chunks</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="nb">bytes</span><span class="p">]],</span> <span class="n">sample_rate</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bytes</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Reconstruct bot audio track with silence gaps between turns preserved.</span><span class="sh">"""</span>
    <span class="n">track</span> <span class="o">=</span> <span class="nf">bytearray</span><span class="p">()</span>
    <span class="n">write_pos</span> <span class="o">=</span> <span class="mi">0</span>  <span class="c1"># bytes
</span>    <span class="k">for</span> <span class="n">offset_secs</span><span class="p">,</span> <span class="n">audio_bytes</span> <span class="ow">in</span> <span class="n">chunks</span><span class="p">:</span>
        <span class="n">wall_pos</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">offset_secs</span> <span class="o">*</span> <span class="n">sample_rate</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span>
        <span class="k">if</span> <span class="n">wall_pos</span> <span class="o">&gt;</span> <span class="n">write_pos</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">wall_pos</span> <span class="o">&gt;</span> <span class="nf">len</span><span class="p">(</span><span class="n">track</span><span class="p">):</span>
                <span class="n">track</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="sa">b</span><span class="sh">"</span><span class="se">\x00</span><span class="sh">"</span> <span class="o">*</span> <span class="p">(</span><span class="n">wall_pos</span> <span class="o">-</span> <span class="nf">len</span><span class="p">(</span><span class="n">track</span><span class="p">)))</span>
            <span class="n">write_pos</span> <span class="o">=</span> <span class="n">wall_pos</span>
        <span class="n">end_pos</span> <span class="o">=</span> <span class="n">write_pos</span> <span class="o">+</span> <span class="nf">len</span><span class="p">(</span><span class="n">audio_bytes</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">end_pos</span> <span class="o">&gt;</span> <span class="nf">len</span><span class="p">(</span><span class="n">track</span><span class="p">):</span>
            <span class="n">track</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="sa">b</span><span class="sh">"</span><span class="se">\x00</span><span class="sh">"</span> <span class="o">*</span> <span class="p">(</span><span class="n">end_pos</span> <span class="o">-</span> <span class="nf">len</span><span class="p">(</span><span class="n">track</span><span class="p">)))</span>
        <span class="n">track</span><span class="p">[</span><span class="n">write_pos</span><span class="p">:</span><span class="n">end_pos</span><span class="p">]</span> <span class="o">=</span> <span class="n">audio_bytes</span>
        <span class="n">write_pos</span> <span class="o">=</span> <span class="n">end_pos</span>
    <span class="k">return</span> <span class="nf">bytes</span><span class="p">(</span><span class="n">track</span><span class="p">)</span>
</code></pre></div></div>

<p>The tricky part: within a single turn, the voice engine streams audio faster than real-time, so consecutive frames land at <code class="language-plaintext highlighter-rouge">wall_pos &lt; write_pos</code> and just append in sequence. Between turns, <code class="language-plaintext highlighter-rouge">wall_pos</code> jumps ahead, inserting exactly the right amount of silence. Get this wrong by a few samples and the entire recording drifts out of sync. I rewrote it three times before the math held.</p>

<p>Two minutes after the recording fix, at 19:18, I pushed a commit titled “made pipeline complete.” 93 insertions across 9 files: the scheduler, the event handlers, the pipeline builder, the service factory, and the WebRTC pipeline all wired together and talking to each other for the first time. The quiet follow-up that made everything actually work.</p>

<p><strong>Bug 2: The race condition that ate the first message</strong></p>

<p>The AI’s opening line was vanishing on every other call. Half the time the bot was a stand-up comedian delivering the opener before the doors opened. Here’s why. Before the AI can speak, two things have to be true: the pipeline has to be booted <em>and</em> the caller has to be connected. If the pipeline boots first and queues the greeting before the caller is on the line, the audio goes nowhere. If the caller connects first, the pipeline isn’t ready yet.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ready_state</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">pipeline_started</span><span class="sh">"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">client_connected</span><span class="sh">"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">triggered</span><span class="sh">"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">_trigger_initial_response</span><span class="p">():</span>
    <span class="nf">if </span><span class="p">(</span>
        <span class="n">ready_state</span><span class="p">[</span><span class="sh">"</span><span class="s">pipeline_started</span><span class="sh">"</span><span class="p">]</span>
        <span class="ow">and</span> <span class="n">ready_state</span><span class="p">[</span><span class="sh">"</span><span class="s">client_connected</span><span class="sh">"</span><span class="p">]</span>
        <span class="ow">and</span> <span class="ow">not</span> <span class="n">ready_state</span><span class="p">[</span><span class="sh">"</span><span class="s">triggered</span><span class="sh">"</span><span class="p">]</span>
    <span class="p">):</span>
        <span class="n">ready_state</span><span class="p">[</span><span class="sh">"</span><span class="s">triggered</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="bp">True</span>
        <span class="k">if</span> <span class="n">first_message</span><span class="p">:</span>
            <span class="k">await</span> <span class="n">task</span><span class="p">.</span><span class="nf">queue_frame</span><span class="p">(</span>
                <span class="nc">TTSSpeakFrame</span><span class="p">(</span><span class="n">first_message</span><span class="p">,</span> <span class="n">append_to_context</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
            <span class="p">)</span>
</code></pre></div></div>

<p>Two events, one gate. Whichever fires second triggers the greeting. It looks obvious now. It took me a day to figure out why the greeting kept disappearing.</p>

<p><strong>Bug 3: The XML tag leak</strong></p>

<p>When an LLM uses a tool, it emits markup like <code class="language-plaintext highlighter-rouge">&lt;function&gt;call_api()&lt;/function&gt;</code> in its output. That’s expected and correct. The problem is when those tags slip through to the voice engine, which dutifully reads them aloud: “less than function call api greater than.” On every single call that triggers a tool.</p>

<p>I wrote a filter that strips them before they reach TTS:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">XMLFunctionTagFilter</span><span class="p">(</span><span class="n">BaseTextFilter</span><span class="p">):</span>
    <span class="n">_PATTERN</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span>
        <span class="sa">r</span><span class="sh">"</span><span class="s">&lt;function[^&gt;]*&gt;.*?&lt;/function&gt;</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">flags</span><span class="o">=</span><span class="n">re</span><span class="p">.</span><span class="n">DOTALL</span> <span class="o">|</span> <span class="n">re</span><span class="p">.</span><span class="n">IGNORECASE</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="k">async</span> <span class="k">def</span> <span class="nf">filter</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_PATTERN</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sh">""</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>
</code></pre></div></div>

<p>Four lines. Zero dependencies. Saved every call from sounding like someone reading HTML out loud.</p>

<p><strong>Bug 4: Deepgram’s empty final transcripts</strong></p>

<p>Speech-to-text engines stream their best guess as you talk, refining it word by word. Deepgram would return a perfectly good interim guess like “Okay.” Then it would return an empty final. Pipecat only creates a transcript event for non-empty finals, so the logic that decides “the human is done talking, go reply” would stall. Voice Activity Detection said the caller stopped, but there was no transcript to act on. The pipeline just waited. Forever.</p>

<p>The fix lives inside a custom STT wrapper I wrote (886 lines in <code class="language-plaintext highlighter-rouge">service_factory.py</code> alone). When a final comes back empty but I have a cached interim, I promote the interim as the real transcript. Deepgram’s docs don’t mention this edge case anywhere. I found it by dumping per-turn WAV files and realizing the engine was hearing audio, producing a transcript, then throwing it away. A waiter who takes your order, reads it back to you, and bins it on the way to the kitchen.</p>

<h3 id="the-echo-problem">The Echo Problem</h3>

<p>When the AI speaks, its voice comes out of the caller’s speaker, bounces around the room, re-enters the microphone, and gets transcribed right back. The AI hears its own voice, treats it as the human talking, generates a reply to itself, speaks that, hears it again. An infinite loop of a robot arguing with its own echo.</p>

<p>The textbook fix is AEC, Acoustic Echo Cancellation: subtract the signal you’re playing from the signal you’re hearing. Done properly it has to account for room acoustics, speaker placement, and mic sensitivity, all of which change on every call. Heavy machinery.</p>

<p>I went a different way. Pipecat’s user aggregator already knows when the bot is speaking and broadcasts mute-start and mute-stop signals. The catch: those signals only announce the mute state, they don’t actually tell the STT engine to stop listening. So I wrote a custom <code class="language-plaintext highlighter-rouge">EchoSuppressionMuteStrategy</code> that acts on them inside the STT layer itself, paired with a <code class="language-plaintext highlighter-rouge">DebouncedExternalUserTurnStartStrategy</code> that adds a cooldown. Even if echo sneaks past and trips the voice detector, the debounce refuses to start a new turn unless a full second has passed since the last one. Two signals, one confirmation. False positives dropped by roughly 90%.</p>

<h3 id="the-voicemail-detector">The Voicemail Detector</h3>

<p>Indian phone networks have their own voicemail greetings, some English, some Hindi. A detector that only knows English patterns is useless for calls in India.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">_PATTERNS</span> <span class="o">=</span> <span class="p">[</span>
    <span class="sa">r</span><span class="sh">"</span><span class="s">please (leave|record) (a|your) message</span><span class="sh">"</span><span class="p">,</span>
    <span class="sa">r</span><span class="sh">"</span><span class="s">not (available|here)</span><span class="sh">"</span><span class="p">,</span>
    <span class="sa">r</span><span class="sh">"</span><span class="s">after the (beep|tone)</span><span class="sh">"</span><span class="p">,</span>
    <span class="sa">r</span><span class="sh">"</span><span class="s">beep ke baad</span><span class="sh">"</span><span class="p">,</span>       <span class="c1"># Hindi: "after the beep"
</span>    <span class="sa">r</span><span class="sh">"</span><span class="s">sandesh chhodiye</span><span class="sh">"</span><span class="p">,</span>   <span class="c1"># Hindi: "please leave a message"
</span><span class="p">]</span>
</code></pre></div></div>

<p>It also has a fallback that needs no language at all: if the voice detector stays active for more than 8 seconds before the first transcript arrives, the other end is almost certainly a recorded greeting, not a person taking a breath. The bot hangs up before it starts leaving a message on someone’s answering machine.</p>

<h3 id="the-empty-response-fallback">The Empty Response Fallback</h3>

<p>Sometimes the LLM returns absolutely nothing. No text, no tool call, no interruption. Just silence. Maybe the prompt confused it. Maybe the provider hiccuped. Maybe a server somewhere sneezed.</p>

<p>Without a fallback, the pipeline sits in dead air. The caller says “hello?” again. Still nothing. They hang up. You just lost a call to literally nothing.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">if </span><span class="p">(</span><span class="ow">not</span> <span class="n">_llm_turn_text_seen</span><span class="p">[</span><span class="sh">"</span><span class="s">value</span><span class="sh">"</span><span class="p">]</span>
        <span class="ow">and</span> <span class="ow">not</span> <span class="n">_llm_turn_tool_called</span><span class="p">[</span><span class="sh">"</span><span class="s">value</span><span class="sh">"</span><span class="p">]</span>
        <span class="ow">and</span> <span class="ow">not</span> <span class="n">_llm_interrupted</span><span class="p">[</span><span class="sh">"</span><span class="s">value</span><span class="sh">"</span><span class="p">]):</span>
    <span class="k">await</span> <span class="n">task</span><span class="p">.</span><span class="nf">queue_frame</span><span class="p">(</span>
        <span class="nc">TTSSpeakFrame</span><span class="p">(</span>
            <span class="sh">"</span><span class="s">I</span><span class="sh">'</span><span class="s">m sorry, I didn</span><span class="sh">'</span><span class="s">t quite get that. Could you repeat yourself?</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">append_to_context</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="p">)</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>Three boolean checks. One apology. <code class="language-plaintext highlighter-rouge">append_to_context=True</code> so the LLM remembers it said this and doesn’t get confused on the next turn. This fires on roughly 3% of calls. Without it, every one of those is a silent failure.</p>

<h3 id="the-webhook-race-condition">The Webhook Race Condition</h3>

<p>At 19:54 that same evening, I pushed the webhook dedupe fix. The race: when a call ends, the pipeline’s cleanup block fires and tries to deliver the call-ended webhook. But the scheduler’s reconciliation loop might find that same unsent webhook and try to deliver it too. Two workers, same payload, hitting the customer’s server twice. The call gets announced as ended twice, which is one more ending than most calls have.</p>

<p>The fix: lease-based dispatch backed by Redis. Before sending, a worker claims the dispatch with a short-lived lock. <code class="language-plaintext highlighter-rouge">claim_webhook_dispatch()</code> atomically checks and sets a “sent” flag. If someone else already claimed it, you skip. After delivery, <code class="language-plaintext highlighter-rouge">mark_webhook_sent()</code>. If all retries exhaust, <code class="language-plaintext highlighter-rouge">mark_webhook_exhausted()</code> so it never loops forever.</p>

<p>Three commits in 40 minutes on a Tuesday evening: the recording fix, the pipeline wiring, and the race condition. That’s what voice infrastructure engineering actually looks like.</p>

<h3 id="the-335-am-commit">The 3:35 AM Commit</h3>

<p>On June 8, at 3:35 AM, I pushed my biggest feature drop. Realtime voice pipelines for the <strong>OpenAI and Azure OpenAI Realtime APIs</strong>, a fundamentally different design where listening, thinking, and speaking all happen inside one model over a single connection, instead of the traditional relay of STT to LLM to TTS. Lower latency, but a completely different shape, so I had to write a separate <code class="language-plaintext highlighter-rouge">build_realtime_pipeline()</code> because the frame graph isn’t the same.</p>

<p>New providers landed in the same commit: <strong>Google Vertex</strong> as another LLM option, <strong>Google TTS and STT</strong>, <strong>Sarvam</strong> STT for Indian languages, <strong>Cartesia</strong> for a faster voice. The <code class="language-plaintext highlighter-rouge">XMLFunctionTagFilter</code>. In-memory audio buffering with background S3 uploads (fire-and-forget tasks held by a strong reference so the garbage collector doesn’t kill an upload mid-flight). A metrics aggregator that tracks prompt tokens, completion tokens, voice characters, and processing time across every stage. And an independent max-duration watchdog as a backstop: if the pipeline somehow never ends a call, a separate task forces it after a timeout.</p>

<p>Then I kept going. Most people sleep after a 3 AM commit. That same day I shipped production configs (a <code class="language-plaintext highlighter-rouge">docker-compose.prod.yml</code> that stops exposing Redis and Postgres on the network, plus a Caddy reverse proxy) and post-call summaries (when an assistant has a <code class="language-plaintext highlighter-rouge">summary_prompt</code>, the transcript goes to the LLM after hang-up and the summary lands in the database). The 3 AM commit was the headline. The production configs are what turned it from a prototype into a product.</p>

<h3 id="production-hardening-and-the-ui-revamp">Production Hardening and the UI Revamp</h3>

<p>June 9 was cleanup day, the kind of day where nothing demos well and everything matters. Webhook retry triggers reworked. The transcript generation got a major overhaul, 214 lines added to <code class="language-plaintext highlighter-rouge">call_transcript.py</code>. Provider columns changed from a strict enum to a plain string, so adding a new provider no longer requires a database migration. Bug fixes across both carriers.</p>

<p>On June 10 at 23:14, I pushed “complete ui revamp.” 52 files changed. 3,512 insertions, 4,229 deletions, net negative because I deleted the old component structure and rebuilt it. Best diff stat of the month. Dashboard, analytics, and settings pages. A real component library: cards, modals, toggles, status dots, empty states, page headers. Every page got its own component instead of being smeared across a dialog, a table, and an edit page. The thing finally looks like a product.</p>

<h3 id="the-architecture-what-actually-runs">The Architecture (What Actually Runs)</h3>

<p>Pipecat does the heavy lifting. One pipeline per call, running as an asyncio Task inside FastAPI. No workers, no threads. A semaphore caps concurrent calls at 50.</p>

<ul>
  <li><strong>Telephony</strong>: Twilio and Plivo via a plugin registry. Adding a carrier means three files and a registration at import time.</li>
  <li><strong>STT</strong>: Deepgram, wrapped in a custom class: interim promotion, echo suppression, per-turn WAV dumps for debugging.</li>
  <li><strong>LLM</strong>: OpenAI, Groq, Google Vertex, Together AI via a factory, plus the OpenAI Realtime API where listen-think-speak collapse into one service.</li>
  <li><strong>TTS</strong>: ElevenLabs, OpenAI, Deepgram, Cartesia, Google, and Sarvam for Indian languages.</li>
  <li><strong>WebRTC</strong>: SmallWebRTCTransport (aiortc under the hood) at 16kHz for browser calls.</li>
  <li><strong>VAD</strong>: Silero, tuned to 0.3s to start and 0.8s to confirm a stop.</li>
  <li><strong>Recording</strong>: dual capture processors with wall-clock offsets, mixed to mono, uploaded to S3.</li>
  <li><strong>State</strong>: Redis for live call state, the transcript buffer (zero database writes while a call is active), and the phone number pool. Postgres and S3 for everything durable.</li>
  <li><strong>Webhooks</strong>: HMAC-SHA256 signing, exponential backoff, Redis lease-based deduplication.</li>
  <li><strong>Crash recovery</strong>: the scheduler resolves orphaned calls on startup, flushes partial transcripts, and returns numbers to the pool.</li>
</ul>

<h3 id="where-we-are-now">Where We Are Now</h3>

<p>Here’s the honest status:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Feature</th>
      <th style="text-align: left">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Real-time voice conversations (sub-second latency)</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">BYOK across LLM, STT, TTS providers</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Recording with proper timeline sync</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Echo suppression with debounced VAD</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Voicemail detection (English + Hindi)</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Vapi-compatible REST API</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Telephony (Twilio, Plivo)</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">WebRTC browser calls</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Webhook system with retry and deduplication</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Realtime voice pipelines (OpenAI/Azure)</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Docker self-hosting</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Dashboard, analytics, settings</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Per-turn millisecond diagnostic metrics</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Crash recovery with orphan cleanup</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Post-call summary generation</td>
      <td style="text-align: left">Working</td>
    </tr>
    <tr>
      <td style="text-align: left">Multi-language STT beyond Hindi</td>
      <td style="text-align: left">In progress</td>
    </tr>
  </tbody>
</table>

<p>One caveat belongs next to that table. This is a one-person, one-month codebase: a single integration test, no CI pipeline, and nothing has pushed it past the 50-call semaphore yet. Every “Working” above means working on real calls, not battle-tested at scale. The QA department is me, my phone, and a 1 AM <code class="language-plaintext highlighter-rouge">docker compose</code> habit.</p>

<h3 id="the-insight">The Insight</h3>

<p>Building a voice AI platform is an exercise in managing complexity at every single layer. The intelligence, the part that sounds impressive in a pitch deck, is a few API calls. The other 90% is the plumbing.</p>

<p>What still surprises me is that effort and impact refuse to correlate. My best code is four lines that strip XML tags from a voice stream. The bug that took the longest was a recording mixer that didn’t know what time it was. And the feature I’m proudest of is a Hindi voicemail pattern that took one line.</p>

<p>This is what real voice infrastructure looks like. It’s not glamorous. But it works.</p>

<hr />

<p><em>The commit messages are vague but the code tells the real story.</em></p>]]></content><author><name>Parth Batra</name></author><category term="ai &amp; tech" /><category term="get-shit-done" /><summary type="html"><![CDATA[You call a number, an AI picks up, you say “hello,” and it says “hello” back. Feels instant. Feels simple. It is neither.]]></summary></entry><entry><title type="html">Your iPhone Doesn’t Improve 20% a Year. Apple Just Tells You It Does.</title><link href="https://fromparth.blog/2026/05/22/your-iphone-is-lying-to-you-about-progress.html" rel="alternate" type="text/html" title="Your iPhone Doesn’t Improve 20% a Year. Apple Just Tells You It Does." /><published>2026-05-22T00:00:00+00:00</published><updated>2026-05-22T00:00:00+00:00</updated><id>https://fromparth.blog/2026/05/22/your-iphone-is-lying-to-you-about-progress</id><content type="html" xml:base="https://fromparth.blog/2026/05/22/your-iphone-is-lying-to-you-about-progress.html"><![CDATA[<p>A friend of mine is doing his PhD in nanotech and semiconductors. His project took almost two years. For the first eighteen months: nothing. Optimization, failure, more optimization. Then one week, devices started working, results came fast, and he had everything he’d been chasing in about three weeks.</p>

<p><a href="https://www.simonandschuster.com/books/Chip-War/Chris-Miller/9781982172008"><em>Chip War</em></a> has a name for this. Chip War’s whole thesis is that real technological progress doesn’t move in straight lines. It staggers. Long flat stretches, then a vertical jump. The history of semiconductors is mostly people failing in rooms for years, then a breakthrough that changes everything at once.</p>

<p>Here’s the trick: companies figured out how to take that jump and sell it back to you as a line.</p>

<p>Say a manufacturing breakthrough gives a 35% improvement in processor performance. They don’t ship all 35% at once. They stage it: 20% this year, 20% next. Same science. Tidier calendar. Science doesn’t have a fiscal quarter — but someone gave it one.</p>

<figure class="diagram">
<svg viewBox="0 0 380 160" width="100%" height="auto" role="img" aria-label="A smooth straight marketing line versus the real progress that stays flat then jumps in one step." font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
  <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
    <path d="M40 140 V24 M40 140 H350" stroke-width="1.8" />
  </g>
  <path d="M40 130 L340 34" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="6 5" opacity="0.5" stroke-linecap="round" />
  <path d="M40 130 H200 V34 H340" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" />
  <g stroke="none" fill="currentColor" font-size="13">
    <text x="300" y="28" opacity="0.6">marketed</text>
    <text x="150" y="128">actual</text>
  </g>
</svg>
<figcaption>The breakthrough lands in one jump. The keynote chart smooths it into a tidy yearly climb.</figcaption>
</figure>

<p>Hardware progress has been slowing for fifteen to twenty years — we’re at the physical limits of how small a transistor can get. The improvements announced now are mostly software. Not the same thing.</p>

<p>Here’s what I keep thinking about: the 18 months were the progress. The week was just when it became legible. We’ve built everything — keynotes, quarterly reports, annual reviews — around the moment something becomes <em>visible</em>, not the moment it becomes <em>real</em>. Those are different moments. Often years apart. We just don’t have a ceremony for the invisible ones.</p>

<p>I upgraded every time they told me to. I trusted the curve.</p>

<hr />

<p>Nobody clapped for those eighteen months. No stage, no announcement. The actual progress happened in a room with bad lighting and probably a lot of bad coffee, and it looked, from the outside, like nothing.</p>

<p>Progress used to be a rupture. An event. Something that happened to the world and left it different. Now it’s a release date. And I’m not sure we can tell the difference anymore — between a thing actually changing, and being told it’s time to notice.</p>

<hr />

<p><em>The breakthrough happened in the bad-lighting room. The keynote is just the ceremony.</em></p>]]></content><author><name>Parth Batra</name></author><category term="just-a-thought" /><category term="book-brain" /><summary type="html"><![CDATA[A friend of mine is doing his PhD in nanotech and semiconductors. His project took almost two years. For the first eighteen months: nothing. Optimization, failure, more optimization. Then one week, devices started working, results came fast, and he had everything he’d been chasing in about three weeks.]]></summary></entry></feed>