<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abeera Lodhi</title>
    <description>The latest articles on DEV Community by Abeera Lodhi (@abeera_lodhi).</description>
    <link>https://dev.to/abeera_lodhi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3831073%2F2370327d-b430-4fc4-bc80-7e75da33b122.png</url>
      <title>DEV Community: Abeera Lodhi</title>
      <link>https://dev.to/abeera_lodhi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abeera_lodhi"/>
    <language>en</language>
    <item>
      <title>I capped extraction at 250 concepts, told the model to obey the cap, and let the app report the result as complete</title>
      <dc:creator>Abeera Lodhi</dc:creator>
      <pubDate>Mon, 24 Aug 2026 02:31:28 +0000</pubDate>
      <link>https://dev.to/abeera_lodhi/i-capped-extraction-at-250-concepts-told-the-model-to-obey-the-cap-and-let-the-app-report-the-1644</link>
      <guid>https://dev.to/abeera_lodhi/i-capped-extraction-at-250-concepts-told-the-model-to-obey-the-cap-and-let-the-app-report-the-1644</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash&lt;/a&gt;: Smash Stories.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; I am building a study app whose entire premise is that it knows what you are about to forget. Extraction caps a source at 250 concepts, for good cost reasons, and I wrote that cap into the system prompt so the model would stop there. It does stop there. Which means a 300 page book comes back as exactly 250 concepts, the hard &lt;code&gt;slice&lt;/code&gt; that would have told me something was dropped never fires, and the screen renders "250 concepts extracted." in the same neutral tone it uses for a two page note. The app was not losing your material by accident. It was losing it exactly as instructed, and then reporting a clean number over the top. The fix is one character, and the version almost everyone would write instead is the version that detects nothing.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Vesprit reads your study material and pulls out testable concepts, then tracks which ones are fading from your memory and puts those in front of you. The fade model only knows about concepts that exist in the database. That matters for what follows.&lt;/p&gt;

&lt;p&gt;Extraction runs in a Cloudflare Worker against Claude, and it has a ceiling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * A ceiling on concepts per source. This is the real cost lever: output tokens
 * cost 5x input, and output scales with concept count rather than source length.
 * Also keeps a single recall backlog from being unusable after one upload.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_CONCEPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment is mine, it is honest, and I still agree with every word of it. The cap is a deliberate design decision, not a bug. Two things follow from it that I did not think about together until much later: the system prompt tells the model to return at most 250, and the parser ends with a hard &lt;code&gt;slice(0, MAX_CONCEPTS)&lt;/code&gt; as a backstop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assumption, stated plainly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A documented, intentional limit is a product constraint, not a defect. If it ever actually bites, the &lt;code&gt;slice&lt;/code&gt; will be the thing that fires, and that is where I would put the detection.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I never wrote that sentence down. That is rather the point. It sat underneath the design as an unexamined belief, and it is wrong in a specific and interesting way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was wrong
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;slice&lt;/code&gt; almost never fires.&lt;/p&gt;

&lt;p&gt;The system prompt instructs the model to stop at 250. On a long source the model does what it is told. It returns exactly 250 concepts, &lt;code&gt;parsed.length&lt;/code&gt; is exactly 250, and &lt;code&gt;slice(0, 250)&lt;/code&gt; removes nothing at all. The backstop I was relying on to notice the problem is a backstop against the model &lt;em&gt;disobeying me&lt;/em&gt;, which is the rare case. The common case, the model obeying, produces a response that is byte for byte indistinguishable from complete coverage of a short source.&lt;/p&gt;

&lt;p&gt;So the boundary was invisible on both sides. The Worker had no idea it had hit a ceiling, because nothing was cut. The app had no idea, because the Worker only ever sent it a &lt;code&gt;concepts&lt;/code&gt; array. The user had no idea, because the screen rendered a count, and a count with no qualifier reads as a total.&lt;/p&gt;

&lt;p&gt;And in this product specifically, the consequence is not "a list is short." Material that never entered the system is invisible to the fade model, so the user gets a confident, well ranked revision queue built over a fraction of their syllabus, while every signal the app gives them says coverage is complete. The app's one job is to know what you are missing. It would have been the thing hiding it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and the one character that carries it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  return parsed.slice(0, MAX_CONCEPTS);
&lt;/span&gt;&lt;span class="gi"&gt;+  const truncated = parsed.length &amp;gt;= MAX_CONCEPTS;
+
+  return { concepts: parsed.slice(0, MAX_CONCEPTS), truncated };
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;gt;=&lt;/code&gt;, not &lt;code&gt;&amp;gt;&lt;/code&gt;. That is the whole thing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; is the version that looks correct in review. It asks "did the slice drop anything," which is the natural question, and it passes a test written by the same person who wrote the check, because that person reaches for the same mental model twice. It would have caught only the case where the model ignored its own instruction, and missed essentially every real truncation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;gt;=&lt;/code&gt; knowingly accepts a false positive. A source that genuinely yields exactly 250 concepts gets flagged as possibly incomplete when it is not. That trade is one sided and I took it on purpose: the cost is one hedged sentence on a rare exact hit, against silently handing someone a fifth of their material and letting them revise from it.&lt;/p&gt;

&lt;p&gt;The reasoning is in the file rather than in a commit message nobody will read again, because the next person to look at this line will feel the same pull toward &lt;code&gt;&amp;gt;&lt;/code&gt; that I did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// -------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="c1"&gt;// WHY `&amp;gt;=` AND NOT `&amp;gt;`&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;// This is the load-bearing line. The system prompt tells the model to return&lt;/span&gt;
  &lt;span class="c1"&gt;// at most MAX_CONCEPTS, so on a large source the model obeys and stops at&lt;/span&gt;
  &lt;span class="c1"&gt;// exactly MAX_CONCEPTS — the slice below never fires, and a `&amp;gt;` test would&lt;/span&gt;
  &lt;span class="c1"&gt;// report nothing. In other words `&amp;gt;` detects only the rare case where the&lt;/span&gt;
  &lt;span class="c1"&gt;// model ignored its own instruction, and misses essentially every real&lt;/span&gt;
  &lt;span class="c1"&gt;// truncation: a 300-page book comes back with 250 concepts, looking for all&lt;/span&gt;
  &lt;span class="c1"&gt;// the world like complete coverage.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests name the case rather than describing the mechanics, which is the only reason I trust them here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flags a response that lands exactly on the ceiling&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// THE case this feature exists for, and the one a `&amp;gt;` comparison misses.&lt;/span&gt;
  &lt;span class="c1"&gt;// The system prompt tells the model to stop at MAX_CONCEPTS, so a large&lt;/span&gt;
  &lt;span class="c1"&gt;// source comes back at exactly the cap with nothing sliced off — which is&lt;/span&gt;
  &lt;span class="c1"&gt;// indistinguishable from complete coverage unless we say so here.&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseConcepts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;concepts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;conceptList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX_CONCEPTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})?.&lt;/span&gt;&lt;span class="nx"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;counts survivors, not raw entries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Malformed entries are dropped before the count is taken, so a payload&lt;/span&gt;
  &lt;span class="c1"&gt;// that only reaches the ceiling by including junk is not a truncation.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withJunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nf"&gt;conceptList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX_CONCEPTS&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expectedAnswer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseConcepts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;concepts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;withJunk&lt;/span&gt; &lt;span class="p"&gt;})?.&lt;/span&gt;&lt;span class="nx"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzebja5dkctcw2unxq8or.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzebja5dkctcw2unxq8or.png" alt="Jest test output showing the parseConcepts block, including the four truncation flag tests, all passing" width="800" height="411"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The first of those names is the whole argument. A &lt;code&gt;&amp;gt;&lt;/code&gt; implementation passes every other test on this screen.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The part of the fix that nearly caused a worse bug
&lt;/h2&gt;

&lt;p&gt;The flag has to persist. Intake navigates straight to the per source view on success, so a toast would be destroyed by the navigation on the exact path large sources arrive by. That meant a new field on the source document, and a new field means a parser change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;     createdAt,
     status,
&lt;span class="gi"&gt;+    // Deliberately NOT part of the validation block above. An older document has
+    // no `truncated` field and must still be a perfectly valid source — treating
+    // its absence as malformed would drop every pre-existing row and blank the
+    // timeline, which is precisely what this function's skip-one-row design
+    // exists to prevent.
+    truncated: data.truncated === true,
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;parseSource&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; for a malformed document and the list path skips nulls, by design, so one corrupt row cannot take down a timeline. Add &lt;code&gt;truncated&lt;/code&gt; to the validation block above that line and every document written before this commit becomes malformed. Every row gets skipped. The user's timeline empties itself, silently, with no error anywhere, which is a considerably worse version of the bug I was in the middle of fixing.&lt;/p&gt;

&lt;p&gt;It is pinned by a test that exists purely to stop someone tidying that comment away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;does NOT reject a document that lacks the field&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The failure this guards against is severe and quiet: treating absence as&lt;/span&gt;
  &lt;span class="c1"&gt;// malformed would make parseSource return null for every pre-existing&lt;/span&gt;
  &lt;span class="c1"&gt;// source, and the list path skips nulls — so the user's timeline would&lt;/span&gt;
  &lt;span class="c1"&gt;// simply empty itself with no error anywhere.&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src_1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the user gets, finally, sits directly under the concept count, because that number is where the belief "this is my material" actually forms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;truncated&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-3 rounded-card border border-line bg-canvas-sunken p-gutter"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-sm font-medium text-ink"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Partial coverage&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-1 text-sm leading-5 text-ink-muted"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      This source held more testable material than one extraction pass covers, so these&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;concepts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; concepts are the most testable ones rather than all of them. Recall
      and fade tracking work normally on what is here.
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not promise a fix. Complete coverage of a long source needs chunked extraction, which is scoped and not built, and offering a remedy that does not exist would be worse than the silence it replaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest note
&lt;/h2&gt;

&lt;p&gt;This was caught by reading my own code while scoping chunked extraction on 2026-08-04, not by a user and not on a device. The app is pre launch, nobody was affected, there was no incident, and no source in the database has ever been long enough to trigger the flag, so the notice above has never rendered outside a fixture. The fix also does nothing until the Worker is redeployed, since the client reads a missing field as &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;An instruction to a model can hide the very boundary it creates.&lt;/strong&gt; I asked for a limit and I got one, cleanly, every time. Obedience is what made this silent. If I had a sloppier model that overran the cap, my &lt;code&gt;slice&lt;/code&gt; backstop would have fired and I would have found this on day one. The better the model followed instructions, the more invisible the problem became, and I do not think I would have predicted that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The obvious implementation of a check is worth one minute of suspicion.&lt;/strong&gt; Not the logic, which was fine, but the question it asks. &lt;code&gt;&amp;gt;&lt;/code&gt; asks "did I cut anything." &lt;code&gt;&amp;gt;=&lt;/code&gt; asks "am I at the edge." Those sound like the same question and they are not, and the difference is the entire feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cap you documented is not a cap the user knows about.&lt;/strong&gt; I had written the number down three times: in a constant, in a comment, in a system prompt. All three were facing me. None of them were facing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silence is a claim.&lt;/strong&gt; Rendering "250 concepts extracted." with no qualifier is not a neutral act. It asserts completeness by omission, and I would never have written that assertion out as a sentence.&lt;/p&gt;




&lt;p&gt;One thing I have not solved, and would genuinely like to hear about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When a model complies with your own constraint, how do you detect that you were at the boundary at all?&lt;/strong&gt; The &lt;code&gt;&amp;gt;=&lt;/code&gt; trick works here because my ceiling is a discrete count I control. It does not generalise to a token budget, a truncated context window, or a summarisation pass where the model quietly compresses instead of stopping. In all of those, obedience and completeness produce the same shaped output, and I do not have a general technique for telling them apart. Is there a real pattern for this, or is everyone hand rolling a sentinel per call site the way I just did?&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I told my own agent the function was memoized. Twice. The device log said otherwise.</title>
      <dc:creator>Abeera Lodhi</dc:creator>
      <pubDate>Mon, 24 Aug 2026 01:42:53 +0000</pubDate>
      <link>https://dev.to/abeera_lodhi/i-told-my-own-agent-the-function-was-memoized-twice-the-device-log-said-otherwise-3be2</link>
      <guid>https://dev.to/abeera_lodhi/i-told-my-own-agent-the-function-was-memoized-twice-the-device-log-said-otherwise-3be2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; My Clerk → Firebase auth bridge finished a full, successful sync and then immediately started over, twice a second, forever, on a real phone. Nothing threw. Every iteration &lt;em&gt;worked&lt;/em&gt;. I was sure the cause was a remount, then I was sure &lt;code&gt;getToken&lt;/code&gt; was memoized, and I had the library source open to prove it. I had the wrong library open. &lt;code&gt;@clerk/clerk-expo&lt;/code&gt; wraps &lt;code&gt;@clerk/clerk-react&lt;/code&gt;'s correctly-memoized &lt;code&gt;getToken&lt;/code&gt; to add an offline JWT cache, and drops the &lt;code&gt;useCallback&lt;/code&gt; doing it. Every render, new identity. Every new identity, another effect run. Every effect run, a &lt;code&gt;setState&lt;/code&gt; that caused the next render. The fix is nine lines. Finding it took building an instrument, because a re-run and a remount look identical in logs and have opposite fixes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Vesprit is a study app. Clerk handles identity, Firestore holds the data, and Firestore's security rules need a Firebase &lt;code&gt;auth.uid&lt;/code&gt;, which Clerk does not give you. So there's a bridge: take the Clerk session token, exchange it at a Cloudflare Worker for a Firebase custom token, sign in to Firebase with that. Three hops, one &lt;code&gt;useEffect&lt;/code&gt;, one status the UI reads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useFirebaseAuthBridge&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;BridgeState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isLoaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isSignedIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSyncStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BridgeStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signed-out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// … getToken() → exchangeForFirebaseToken() → signInWithCustomToken()&lt;/span&gt;
    &lt;span class="c1"&gt;// … setSyncStatus('syncing') … setSyncStatus('ready')&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isSignedIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getToken&lt;/code&gt; is in the dependency array because the lint rule says it should be, and because Clerk's own documentation shows &lt;code&gt;getToken&lt;/code&gt; used inside effects. That is the entire bug, sitting in plain sight, looking like correct code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faf5qtm2soj8c9tfdt8bd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faf5qtm2soj8c9tfdt8bd.jpeg" alt="Database connection status cycling between Connecting and ready" width="720" height="598"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Not a stall. This row is completing successfully and then starting again.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What was wrong
&lt;/h2&gt;

&lt;p&gt;On device, the terminal read like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[firebase-bridge +2ms]   requesting Clerk session token
[firebase-bridge +6ms]   exchanging Clerk token for a Firebase custom token
[firebase-bridge +67ms]  signing in to Firebase with the custom token
[firebase-bridge +716ms] ready — Firestore rules will now see request.auth.uid
[firebase-bridge +2ms]   requesting Clerk session token          &amp;lt;- and again, forever
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that last line carefully, because it's what made this hard. The sync &lt;strong&gt;succeeded&lt;/strong&gt;. It reached &lt;code&gt;ready&lt;/code&gt;. Then it did the whole thing again, roughly twice a second, indefinitely.&lt;/p&gt;

&lt;p&gt;Every one of those cycles minted a real Firebase custom token: a network call to Clerk, an invocation of my Cloudflare Worker, and a Firebase sign-in. On a phone. On battery. There was no error state to find, no exception to catch, no failed request in the network log. The app was working perfectly, several times a second, forever.&lt;/p&gt;

&lt;p&gt;This is the class of bug that error tracking cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two things I was sure about, and why both were wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wrong hypothesis #1: "The effect is re-running and cancelling its own result."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was the first theory, and it was mine and my agent's together. It's the standard shape: an effect re-fires, the cleanup sets &lt;code&gt;cancelled = true&lt;/code&gt;, and the in-flight result gets discarded before it can land.&lt;/p&gt;

&lt;p&gt;Disproved by absence. The &lt;code&gt;console.error&lt;/code&gt; in the bridge's catch block sits &lt;em&gt;outside&lt;/em&gt; the &lt;code&gt;cancelled&lt;/code&gt; guard, so it fires unconditionally whenever the catch is reached. It never fired. Nothing was throwing. Silence from that line was the whole diagnosis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong hypothesis #2: "&lt;code&gt;getToken&lt;/code&gt; is memoized, so the dependency is stable."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one worth writing the post about. I said this confidently. I had evidence. Here is the evidence I had, from &lt;code&gt;@clerk/clerk-react&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createGetToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isomorphicClerk&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isomorphicClerk&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memoized on a stable singleton. Stable identity. The effect runs once. Case closed. The loop must be a remount, which is a problem one layer &lt;em&gt;above&lt;/em&gt; this hook, in how the provider is mounted.&lt;/p&gt;

&lt;p&gt;I was reading the wrong package.&lt;/p&gt;

&lt;p&gt;The app imports from &lt;code&gt;@clerk/clerk-expo&lt;/code&gt;. That's a different package with its own &lt;code&gt;useAuth&lt;/code&gt;, and I had generalised from the source of a library the app does not import.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instrument, because logs couldn't answer it
&lt;/h2&gt;

&lt;p&gt;Here's the thing that made this more than a lucky guess: &lt;strong&gt;a re-run and a remount produce identical logs&lt;/strong&gt;, and they have opposite fixes. A changed dependency is fixed inside the hook. A remount is fixed above it. I had a 50/50 and no way to settle it by reading.&lt;/p&gt;

&lt;p&gt;So before fixing anything, I built something to tell them apart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/dep-trace.ts&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// `nextInstanceId` is module scope on purpose. Component-local state cannot&lt;/span&gt;
&lt;span class="c1"&gt;// distinguish "this instance ran twice" from "two instances each ran once",&lt;/span&gt;
&lt;span class="c1"&gt;// which is precisely the ambiguity in a paired log.&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;nextInstanceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;claimInstanceId&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nextInstanceId&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;diffDependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FIRST RUN (fresh mount — if you see this repeatedly, the component is remounting)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RE-RUN with NO changed dependency (double-invoke or Fast Refresh)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`RE-RUN — changed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Module scope is the entire point. If the counter lived in component state it would reset with the component, and "instance #1 ran twice" would be indistinguishable from "instance #1 and instance #1", which is exactly the question being asked.&lt;/p&gt;

&lt;p&gt;Wired into the bridge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+  const [instanceId] = useState(claimInstanceId);
+  const previousDepsRef = useRef&amp;lt;readonly unknown[] | null&amp;gt;(null);
+
+  useEffect(() =&amp;gt; {
+    if (__DEV__) console.log(`[firebase-bridge #${instanceId}] MOUNTED`);
+    return () =&amp;gt; {
+      if (__DEV__) console.log(`[firebase-bridge #${instanceId}] UNMOUNTED`);
+    };
+  }, [instanceId]);
+
&lt;/span&gt;   useEffect(() =&amp;gt; {
&lt;span class="gi"&gt;+    if (__DEV__) {
+      const deps = [isLoaded, isSignedIn, userId, getToken, attempt];
+      console.log(
+        `[firebase-bridge #${instanceId}] effect: ` +
+          diffDependencies(previousDepsRef.current, deps, [
+            'isLoaded', 'isSignedIn', 'userId', 'getToken', 'attempt',
+          ]),
+      );
+      previousDepsRef.current = deps;
+    }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail I'd have got wrong a year ago: the instance id comes from a &lt;strong&gt;lazy &lt;code&gt;useState&lt;/code&gt;&lt;/strong&gt;, not a &lt;code&gt;useRef&lt;/code&gt;. Reading a ref during render violates the Rules of React, and this component is compiled by React Compiler, which is entitled to assume those rules hold. A lazy initializer runs exactly once per instance and is safe to read while rendering.&lt;/p&gt;

&lt;p&gt;One device run answered it. Every line read &lt;code&gt;#1&lt;/code&gt;: the component never remounted, not once. And every effect run read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[firebase-bridge #1] effect: RE-RUN — changed: getToken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0p1k58h0hh9v946wt7h.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0p1k58h0hh9v946wt7h.jpeg" alt="Terminal log showing repeated RE-RUN — changed: getToken lines" width="799" height="218"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The instrument naming the dependency I had just finished arguing was stable.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@clerk/clerk-expo@2.19.31&lt;/code&gt;, &lt;code&gt;dist/hooks/useAuth.js&lt;/code&gt;, verbatim, with only the bundler's CommonJS preamble stripped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;import_clerk_react&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@clerk/clerk-react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;import_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@clerk/shared/error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;import_cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAuth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialAuthState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getTokenBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;import_clerk_react&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;initialAuthState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getTokenBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;import_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SessionJWTCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkInit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;import_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SessionJWTCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;import_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SessionJWTCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;import_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SessionJWTCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;import_error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isNetworkError&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;import_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SessionJWTCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getToken&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That first line is the one I misread for days. It looks recursive (&lt;code&gt;useAuth&lt;/code&gt; calling &lt;code&gt;useAuth&lt;/code&gt;), but the one on the right is &lt;code&gt;import_clerk_react.useAuth&lt;/code&gt;, a &lt;em&gt;different package's&lt;/em&gt; hook pulled in under an internal name. In a post whose entire moral is "read the right source," it would be poor form to paraphrase this, so that's the literal file.&lt;/p&gt;

&lt;p&gt;clerk-expo takes clerk-react's properly-memoized &lt;code&gt;getToken&lt;/code&gt;, wraps it to add an offline JWT cache so tokens survive going offline (a genuinely good feature), and returns the wrapper. There is no &lt;code&gt;useCallback&lt;/code&gt; around the wrapper. New function identity, every render.&lt;/p&gt;

&lt;p&gt;The memoization still exists. It's just one layer down, wrapped in something that isn't memoized, which makes it worthless to anyone downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And no, this isn't fixed, and I'm not the first to notice the ingredient.&lt;/strong&gt; &lt;code&gt;@clerk/clerk-expo&lt;/code&gt; is deprecated now (Clerk replaced it with &lt;code&gt;@clerk/expo&lt;/code&gt; in Core 3), so a defect in it would be a fair thing to shrug at. I checked the replacement. &lt;code&gt;@clerk/expo@4.5.2&lt;/code&gt;, &lt;code&gt;dist/hooks/useAuth.js&lt;/code&gt;, as of &lt;strong&gt;2026-08-24&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAuth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getTokenBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_clerk_react&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getTokenBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* SessionJWTCache save/remove */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* offline fallback */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getToken&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New package, new build tooling, same wrapper, same missing &lt;code&gt;useCallback&lt;/code&gt;. The file contains zero occurrences of &lt;code&gt;useCallback&lt;/code&gt; or &lt;code&gt;useMemo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The un-memoized identity is also already corroborated in the wild, which I'd rather say than pretend I found it alone: &lt;a href="https://github.com/get-convex/convex-js/issues/176" rel="noopener noreferrer"&gt;get-convex/convex-js#176&lt;/a&gt; (July 2026) ships an &lt;code&gt;eslint-disable-next-line react-hooks/exhaustive-deps&lt;/code&gt; whose comment states it flatly: &lt;em&gt;Clerk's Expo &lt;code&gt;useAuth&lt;/code&gt; does not memoize &lt;code&gt;getToken&lt;/code&gt;&lt;/em&gt;. So the fact is known. It's just known in a workaround comment inside somebody else's repository, not in an issue on Clerk's tracker, where I couldn't find it at all.&lt;/p&gt;

&lt;p&gt;What I haven't seen written down anywhere is the part below: that when the effect also sets state, the missing memoization stops being a lint annoyance and becomes a self-sustaining loop that mints real credentials several times a second and never once looks like a failure. That consequence is my own diagnosis, and it's the reason a known-but-under-reported trap is worth a post.&lt;/p&gt;

&lt;p&gt;And because the effect calls &lt;code&gt;setSyncStatus&lt;/code&gt;, the loop needs nothing external to sustain it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;effect runs → setSyncStatus → re-render → new getToken identity → effect runs → …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a perpetual motion machine made of correct-looking code. Any &lt;code&gt;useEffect&lt;/code&gt; that lists Clerk's Expo &lt;code&gt;getToken&lt;/code&gt; in its dependencies &lt;em&gt;and&lt;/em&gt; sets state has this, on the deprecated package and the current one alike. Clerk's docs show &lt;code&gt;getToken&lt;/code&gt; used inside effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  const { isLoaded, isSignedIn, userId, getToken } = useAuth();
&lt;/span&gt;&lt;span class="gi"&gt;+  // `getToken` is destructured under a different name and immediately stabilised.
+  // @clerk/clerk-expo rebuilds it on EVERY render (it wraps clerk-react's memoized
+  // version to add an offline JWT cache and does not re-memoize), so using it
+  // directly in the dependency array below re-runs the effect on every render —
+  // and since the effect calls setSyncStatus, it re-renders itself, forever.
+  const { isLoaded, isSignedIn, userId, getToken: getTokenUnstable } = useAuth();
+  const getToken = useStableCallback(getTokenUnstable);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the hook itself, the "latest ref" pattern, which is React's own &lt;code&gt;useEffectEvent&lt;/code&gt; proposal hand-rolled while that stays experimental:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useStableCallback&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Args&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// No dependency array on purpose: this must run after EVERY render, since the&lt;/span&gt;
  &lt;span class="c1"&gt;// whole premise is that `callback` changes identity every time.&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Empty deps, so the identity handed to callers never changes.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine lines of body. Days of being confidently wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix I didn't make, which is the interesting one
&lt;/h3&gt;

&lt;p&gt;The obvious move is to delete &lt;code&gt;getToken&lt;/code&gt; from the dependency array. The lint rule goes quiet, the loop stops, you move on.&lt;/p&gt;

&lt;p&gt;That installs a worse bug than the one you fixed. The effect then closes over whichever &lt;code&gt;getToken&lt;/code&gt; existed at mount and calls that one forever. After Clerk refreshes the session, the mount-time &lt;code&gt;getToken&lt;/code&gt; is stale, so your bridge starts minting Firebase tokens from an expired Clerk session, and now you have an auth failure that only appears after the app has been open long enough to refresh. Good luck reproducing that.&lt;/p&gt;

&lt;p&gt;The distinction is a test, not a comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;calls the LATEST callback, not the one captured at mount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The reason this is not just "delete the dependency". A frozen closure would&lt;/span&gt;
  &lt;span class="c1"&gt;// keep calling the mount-time getToken and hand back a stale session token&lt;/span&gt;
  &lt;span class="c1"&gt;// after Clerk refreshes.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rerender&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useStableCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;initialProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rerender&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;second&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;second&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus one for the loop itself, and one for a re-render arriving mid-flight while a token exchange is in the air:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keeps one identity even when the input changes on every render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// This is the exact shape of the bug.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rerender&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useStableCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rerender&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rerender&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;survives a re-render mid-flight, still resolving the original call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rerender&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useStableCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;initialProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rerender&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;second&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;resolves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's one sharp edge worth documenting, and it's in the file: &lt;strong&gt;do not call the returned function during render.&lt;/strong&gt; It reads a ref updated in an effect, so during render it hands back the previous render's callback. Effects, event handlers, and async work only.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffv4th3bkbh03czqrlcly.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffv4th3bkbh03czqrlcly.jpeg" alt="Terminal log showing one clean run through to ready with nothing after it" width="796" height="80"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The same screen after. The interesting part is the empty space underneath.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest scale
&lt;/h2&gt;

&lt;p&gt;I want to be straight about how big this was, because the temptation to inflate it is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody was affected.&lt;/strong&gt; Vesprit is pre-launch: no users, no store listing yet. This was caught on my own device during development, on a build only I was running. Nothing leaked, nothing was corrupted, no one's battery died.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was my bug too, not purely Clerk's.&lt;/strong&gt; Clerk's wrapper is the defect, but I put &lt;code&gt;getToken&lt;/code&gt; in a dependency array and then reasoned about it from the wrong package's source. The library made the trap; I walked into it and then argued the trap wasn't there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is a workaround, not a patch, and the trap is still open.&lt;/strong&gt; I have an upstream report drafted against &lt;code&gt;clerk/javascript&lt;/code&gt;, retargeted at &lt;code&gt;@clerk/expo@4.5.2&lt;/code&gt; now that I've confirmed the current package still ships it, and I haven't filed it yet. Until I do, this is still there for everyone else, which is the part that actually bothers me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I did not discover the ingredient, only the consequence.&lt;/strong&gt; The missing memoization is already documented, in passing, in an &lt;code&gt;eslint-disable&lt;/code&gt; comment in &lt;a href="https://github.com/get-convex/convex-js/issues/176" rel="noopener noreferrer"&gt;convex-js#176&lt;/a&gt;. I'd rather say that than sell this as a lone discovery. What I contribute is the diagnosis of what it does when the effect sets state, the instrument that distinguishes it from a remount, and a fix that doesn't quietly swap it for a stale-token bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's real:&lt;/strong&gt; the loop was a full token mint several times a second on a mobile device, indefinitely, and it would have shipped. The pattern Clerk's own documentation shows still produces it in the current package, and the people who have it will not see it, because nothing about it looks or logs like a failure.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;When two causes produce identical logs, build the instrument before you build the fix.&lt;/strong&gt; I could have shipped a plausible guess in ten minutes and been wrong in a way I wouldn't have discovered for weeks. The instrument was thirty lines and settled it in one device run. It's still in the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading the source is evidence. Reading the &lt;em&gt;right&lt;/em&gt; source is the evidence.&lt;/strong&gt; I did the thing that feels most rigorous (go read the library), and it made me more confident and more wrong at the same time. &lt;code&gt;@clerk/clerk-react&lt;/code&gt; and &lt;code&gt;@clerk/clerk-expo&lt;/code&gt; are different packages, and my agent and I both generalised from the one that wasn't installed. Now I check &lt;code&gt;node_modules&lt;/code&gt; for the package name in the import statement, not the package I assume is underneath.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Success is not a signal that things are fine.&lt;/strong&gt; Every metric in this system was green. Every request returned 200. Every sync completed. The bug was that it completed &lt;em&gt;again&lt;/em&gt;. I've since started thinking about "how often does this correct thing happen" as a first-class question, not just "does it work."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A memoization one layer down is not a memoization.&lt;/strong&gt; If you wrap a stable callback, you own its stability now.&lt;/p&gt;




&lt;p&gt;Two things I'd genuinely like answers to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Has anyone found a way to catch this class of bug automatically?&lt;/strong&gt; Not the loop itself: a render-count or effect-count budget in dev would catch that. I mean the general case: an operation that is &lt;em&gt;individually correct&lt;/em&gt; and &lt;em&gt;collectively pathological&lt;/em&gt;. Every tool I have is built around things going wrong, and this never went wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And how do you handle the un-memoized-callback-from-a-library problem at scale?&lt;/strong&gt; &lt;code&gt;useStableCallback&lt;/code&gt; fixes one call site. There are dozens of hooks across the ecosystem returning fresh identities every render, and the honest answer for most of them is "wrap it and hope you noticed." Is there something better than a per-site workaround?&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The Waiting Room: I generated a voice for eight shelter dogs from their own paperwork</title>
      <dc:creator>Abeera Lodhi</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:40:02 +0000</pubDate>
      <link>https://dev.to/abeera_lodhi/the-waiting-room-i-generated-a-voice-for-eight-shelter-dogs-from-their-own-paperwork-4i60</link>
      <guid>https://dev.to/abeera_lodhi/the-waiting-room-i-generated-a-voice-for-eight-shelter-dogs-from-their-own-paperwork-4i60</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-08-13"&gt;Weekend Challenge: Dog Days Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I really enjoy the company of my human friends. I may be shy at first, but once we get to know each other I will shower you with love! All I need is a soft warm place to lay, and to be told I am the goodest boy. I already know how to sit and I walk lovely on leash!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's Hart's entire adoption listing. Read it again and tell me one thing about him.&lt;/p&gt;

&lt;p&gt;You can't. Not his age, not his size, not how long he has been sitting in a kennel. And here is the part that took me most of a day to notice: &lt;strong&gt;two of those four sentences are not about Hart at all.&lt;/strong&gt; They also appear, word for word, in other dogs' listings at the same shelter.&lt;/p&gt;

&lt;p&gt;Most listening projects would take that text and read it in a nicer voice. &lt;strong&gt;The Waiting Room reads it twice — once in the shelter's own upbeat register, and once in a voice generated from Hart's structured record — and the gap between the two is the whole argument.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The Waiting Room&lt;/strong&gt; is a single-page listening study of eight real, adoptable dogs. Every word on every card is verbatim from the shelter that published it. The only thing I added is the voice.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Eight real shelter listings, each read twice. The flat reading is a stock ElevenLabs voice. The second reading is a voice &lt;strong&gt;generated by ElevenLabs Voice Design from a prompt that a documented function computes out of that dog's record&lt;/strong&gt; — age band, size, days waiting, returns, how it arrived. Flipping between the two readings preserves the playhead, so the same sentence changes voice mid-word. Not one word of the listing text is mine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://waiting-room-ruby.vercel.app/" rel="noopener noreferrer"&gt;https://waiting-room-ruby.vercel.app/&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Abeera81/Waiting-room" rel="noopener noreferrer"&gt;https://github.com/Abeera81/Waiting-room&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/k8hiKmGivms"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;60-second walkthrough — the audio swap is the thing to listen for.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The whole interaction fits in one screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://waiting-room-ruby.vercel.app/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgyj9urhjte967plm9fkb.png" alt="The desk, with Yuji's record on top of the deck" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One record at a time on a desk, with the rest of the deck showing underneath.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press play. You hear the listing in a flat, upbeat listing voice.&lt;/li&gt;
&lt;li&gt;Mid-sentence, hit &lt;strong&gt;DESIGNED&lt;/strong&gt;. The words continue from exactly where they were, in a different voice.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;How this voice was derived&lt;/strong&gt; to see which attribute produced which part of the prompt.&lt;/li&gt;
&lt;li&gt;On Yuji, drag the &lt;strong&gt;stay dial&lt;/strong&gt; from Week 1 to Year 2 and listen to the same sentence get tired.&lt;/li&gt;
&lt;li&gt;Any sentence with an amber underline is one the shelter also gave to another dog — click a name to hear that dog read the same words.&lt;/li&gt;
&lt;li&gt;Arrow keys, drag, or the tabs at the bottom move through all eight records.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The finding: the template is louder than the dogs
&lt;/h2&gt;

&lt;p&gt;I did not set out to write about templates. I set out to build a voice generator and needed eight real listings to feed it. Then I read them side by side.&lt;/p&gt;

&lt;p&gt;Three of the eight dogs were given this sentence, character for character:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I may be shy at first, but once we get to know each other I will shower you with love!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sterling is an 8-year-old Chihuahua. Ponyboy is a 3-year-old Anatolian Shepherd. Hart is a 9-year-old pit bull. One template, three animals with nothing in common but a shelter.&lt;/p&gt;

&lt;p&gt;It isn't a one-off. The page detects the overlaps itself at load — exact string match after collapsing whitespace, six-word minimum, no fuzzy matching — and finds &lt;strong&gt;three shared sentences across five of the eight dogs&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shared sentence&lt;/th&gt;
&lt;th&gt;Appears in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"I may be shy at first, but once we get to know each other I will shower you with love!"&lt;/td&gt;
&lt;td&gt;Sterling, Ponyboy, Hart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"I can be worried about new people, new surroundings and touch."&lt;/td&gt;
&lt;td&gt;Sun Bear, Sterling, Ponyboy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"All I need is a soft warm place to lay, and to be told I am the goodest boy."&lt;/td&gt;
&lt;td&gt;Ponyboy, Hart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ponyboy comes off worst. &lt;strong&gt;Three of the four sentences in his listing belong to other dogs too.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://waiting-room-ruby.vercel.app/post/images/07-shared-listing.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9p0tshau1f42er2lseh4.png" alt="Ponyboy's listing with three of four sentences underlined as shared" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The marks are drawn from the detector, not typed by me. The underlined sentences are the ones that are not his.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I want to be fair to the shelters here: a template is a rational response to being understaffed. Oregon Humane is moving hundreds of animals with a handful of people. But the effect is that the document cannot tell a 3-year-old Anatolian Shepherd from a 9-year-old pit bull, and the document is what an adopter reads.&lt;/p&gt;

&lt;p&gt;That is not a claim I have to argue. It's in the source material, and the page checks it in front of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thesis
&lt;/h2&gt;

&lt;p&gt;Adoption is an emotional decision made from a bureaucratic artifact.&lt;/p&gt;

&lt;p&gt;The listing text is 100% the shelter's — no additions, no paraphrase, no trimming for taste. The shelters write in the dog's first person, cheerfully, and that cheerfulness ships exactly as published. The only thing I contribute is the voice.&lt;/p&gt;

&lt;p&gt;So when a worn, slow voice reads &lt;em&gt;"I will shower you with love!"&lt;/em&gt;, nothing has been editorialised. &lt;strong&gt;The cheerfulness is the institution's. The weariness is the record's.&lt;/strong&gt; Both were already in the document; they were just never audible at the same time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://waiting-room-ruby.vercel.app/post/images/03-voice-flat.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faz3r7npkxr8gn53yw4l5.png" alt="The FLAT / DESIGNED switch in flat mode" width="646" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The shelter's reading.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://waiting-room-ruby.vercel.app/post/images/04-voice-designed.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozh1tz6f8xp2js9a8brf.png" alt="The same switch in designed mode, amber" width="646" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The record's reading. Same words, same file length, same playhead.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Abeera81" rel="noopener noreferrer"&gt;
        Abeera81
      &lt;/a&gt; / &lt;a href="https://github.com/Abeera81/Waiting-room" rel="noopener noreferrer"&gt;
        Waiting-room
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;The Waiting Room&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Shelter listings are documents. The Waiting Room makes them sound like the dogs they describe.&lt;/p&gt;
&lt;p&gt;Built for the DEV Weekend Challenge: Dog Days Edition (Best Use of ElevenLabs)
Repository created 2026-08-16, within the challenge window.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run locally&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;node tools/serve.js 8124   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; then open http://localhost:8124&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Use this rather than &lt;code&gt;python -m http.server&lt;/code&gt;: Chrome's media stack issues ranged
requests for audio and stalls against a server that ignores them.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tests&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm &lt;span class="pl-c1"&gt;test&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;31 tests, no dependencies (&lt;code&gt;node:test&lt;/code&gt;). They cover the mapping function, the
shared-sentence detector, and the shipped &lt;code&gt;data/dogs.json&lt;/code&gt; itself — that every
audio file referenced exists, that all eight dogs derive distinct prompts, that
no slider stop duplicates its neighbour, and that no placeholder text survived.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/voicePrompt.js&lt;/code&gt; — the attribute → prompt mapping. Pure function
&lt;code&gt;buildVoicePrompt(attributes)&lt;/code&gt; returns the string handed to ElevenLabs Voice
Design; &lt;code&gt;derive()&lt;/code&gt; returns the same mapping with every rule's…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Abeera81/Waiting-room" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;No backend, no database, no build step. Four modules and a stylesheet: &lt;code&gt;voicePrompt.js&lt;/code&gt; (the mapping), &lt;code&gt;audioBus.js&lt;/code&gt; (the swap), &lt;code&gt;sharedText.js&lt;/code&gt; (the template detector), &lt;code&gt;app.js&lt;/code&gt; (the deck), and &lt;code&gt;styles.css&lt;/code&gt;. Thirty-two tests under &lt;code&gt;node --test&lt;/code&gt;, zero dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  The swap is the demo
&lt;/h3&gt;

&lt;p&gt;Two audio files, one &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element, and the playhead held across the source change. This is the moment I built first and everything else around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/audioBus.js — the same sentence, in a different voice, mid-word&lt;/span&gt;
&lt;span class="nf"&gt;swapSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSrc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Mid-drag the element has already been reset by an earlier swap, so trust&lt;/span&gt;
  &lt;span class="c1"&gt;// the held intent over what the element currently reports.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wasPlaying&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasPlaying&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;swapToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wasPlaying&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;restore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;swapToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// a newer swap already won&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wasPlaying&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loadedmetadata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured seek error across three consecutive flat↔designed swaps: &lt;strong&gt;0.000 s&lt;/strong&gt;, about 35 ms to load.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pending&lt;/code&gt; and &lt;code&gt;swapToken&lt;/code&gt; bookkeeping is not defensive decoration — it's two real bugs. Assigning &lt;code&gt;.src&lt;/code&gt; immediately resets &lt;code&gt;currentTime&lt;/code&gt; to zero, so a naive version reads that zero when a second swap arrives before the first has loaded, and the playhead is gone. That is exactly what dragging the stay dial does. The token exists because a stale &lt;code&gt;loadedmetadata&lt;/code&gt; handler from a superseded swap will happily rewind you to a position you left three swaps ago. (There was also a third bug, which was mine and not the browser's: a hand-built silent placeholder MP3 with its channel-mode bits in the wrong byte, undecodable in Chrome and perfectly decodable everywhere I tested first.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The hero dog: one variable moved
&lt;/h2&gt;

&lt;p&gt;Yuji is a senior American Staffordshire Terrier and Labrador mix at Better World Rescue. He was hit by a car and surrendered by his family. His listing says he has been waiting "way too long".&lt;/p&gt;

&lt;p&gt;His card carries a &lt;strong&gt;stay dial&lt;/strong&gt; with four stops. Every stop calls the same function on the same record, with &lt;code&gt;days_in_shelter&lt;/code&gt; as the only thing that changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stop&lt;/th&gt;
&lt;th&gt;&lt;code&gt;days_in_shelter&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;What the function adds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Week 1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(nothing)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 1&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;a little flat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 4&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;quiet and tired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 2&lt;/td&gt;
&lt;td&gt;730&lt;/td&gt;
&lt;td&gt;flat, barely lifting, worn through&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dragging the dial swaps the audio through the same playhead-preserving function, so the voice ages mid-sentence. Four stops, four different weariness bands — chosen so that no drag is silent, and a test fails loudly if anyone changes the stops in a way that makes two of them collide.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://waiting-room-ruby.vercel.app/post/images/05-dial-year2.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81cthjunh185szsskljg.png" alt="The stay dial at Year 2 with the derived prompt beside it" width="800" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The readout updates from the function, not from a lookup table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the label on that dial before you read anything into it.&lt;/strong&gt; It is a demonstration of what the mapping does with one variable. It is not a claim about Yuji's actual stay. More on that below, because it turned out to be the most interesting thing I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;This is the intellectual core, so here it is in full. Five rules, applied in order, concatenated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BASE       age_band     puppy  → "bright, very fast, unsteady, tumbling"
                        young  → "quick, energetic, eager"
                        adult  → "even, steady, measured"
                        senior → "low, slow, weary, patient"
PITCH      size         large  → "lower, resonant"     small → "higher, clipped"
                        medium → (nothing — the unmarked case)
WEARINESS  days         1-29   → (nothing)             30-119  → "a little flat"
                        120-179→ "quiet and tired"     180+    → "flat, barely lifting, worn through"
GUARD      returns      1      → "slightly hesitant"   2+      → "anxious, over-eager, trying too hard"
STRAIN     intake       stray  → "watchful"            owner_surrender → "resigned"
                        returned → "uncertain"         unknown → (nothing)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a real function, not a hardcoded lookup — &lt;code&gt;derive()&lt;/code&gt; returns the same mapping with every rule's input and output, and the card renders that live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/voicePrompt.js — pure, no I/O, no randomness, input not mutated&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildVoicePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;derive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fired&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;derive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;RULES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`buildVoicePrompt: attributes.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is required`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="na"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fired&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fired&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fired&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every card shows its own working, including the rules that fired &lt;em&gt;nothing&lt;/em&gt; — because a rule that stays silent because the shelter published nothing is a different thing from a rule that had nothing to say, and the panel labels which is which:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://waiting-room-ruby.vercel.app/post/images/06-derivation.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frrynqm6h6mj9r9q0gx8r.png" alt="The derivation panel showing five rules and the final prompt" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The record on the left, the prompt on the right, no step hidden.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Eight records in, eight distinct prompts out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dog&lt;/th&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;Generated prompt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Yuji&lt;/td&gt;
&lt;td&gt;senior, large, owner surrender&lt;/td&gt;
&lt;td&gt;low, slow, weary, patient, lower, resonant, resigned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paddington&lt;/td&gt;
&lt;td&gt;adult, large, returned once&lt;/td&gt;
&lt;td&gt;even, steady, measured, lower, resonant, slightly hesitant, uncertain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sun Bear&lt;/td&gt;
&lt;td&gt;puppy (3 mo), medium&lt;/td&gt;
&lt;td&gt;bright, very fast, unsteady, tumbling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sterling&lt;/td&gt;
&lt;td&gt;senior, small&lt;/td&gt;
&lt;td&gt;low, slow, weary, patient, higher, clipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buck&lt;/td&gt;
&lt;td&gt;young, small&lt;/td&gt;
&lt;td&gt;quick, energetic, eager, higher, clipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moose&lt;/td&gt;
&lt;td&gt;young, large&lt;/td&gt;
&lt;td&gt;quick, energetic, eager, lower, resonant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ponyboy&lt;/td&gt;
&lt;td&gt;adult, large&lt;/td&gt;
&lt;td&gt;even, steady, measured, lower, resonant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hart&lt;/td&gt;
&lt;td&gt;senior, large&lt;/td&gt;
&lt;td&gt;low, slow, weary, patient, lower, resonant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sterling and Hart are both seniors. One is a Chihuahua and one is a pit bull, and the size rule is the only reason they don't sound the same. The shelter's copy could not tell them apart. The record could.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Voice Design, and not a preset voice with tuned parameters
&lt;/h2&gt;

&lt;p&gt;Picking a preset is choosing from a menu. Voice Design generates a novel voice from a text description — which means the &lt;em&gt;record&lt;/em&gt; can produce the voice, because the record can produce the description. That's the entire mechanism, and it's why this is a Voice Design project rather than a TTS project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now let me name the control honestly, because the obvious objection is a good one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The flat reading is not a monotone robot. It's ElevenLabs' stock &lt;strong&gt;"Roger"&lt;/strong&gt; — their own description is &lt;em&gt;"Charismatic, positive and conversational"&lt;/em&gt; — on &lt;strong&gt;Eleven Multilingual v2&lt;/strong&gt;, the same model as every designed voice. I deliberately did not sandbag the control. Roger is the upbeat register that shelter marketing is already written in, which makes the comparison one of &lt;strong&gt;tone, not expressiveness&lt;/strong&gt;. If I'd used a flat robotic preset, the demo would be more dramatic and would prove nothing.&lt;/p&gt;

&lt;p&gt;Nineteen clips, all generated on the free tier, which ended with 116 credits to spare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the audio is pre-generated
&lt;/h2&gt;

&lt;p&gt;Every clip is committed to the repo as an MP3. The browser makes no API calls and the repo holds no keys.&lt;/p&gt;

&lt;p&gt;Three reasons, in order of how much I care: a judge opening this in six months hears exactly what I heard; there is no key to leak in a public repo; and no rate limit can decide whether my submission works during judging. The cost is that the demo can't voice arbitrary new text — which is the correct trade for a piece whose entire point is eight specific records.&lt;/p&gt;

&lt;h2&gt;
  
  
  The variable I could not get
&lt;/h2&gt;

&lt;p&gt;Here is the part I did not expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No shelter publishes an intake date.&lt;/strong&gt; Not one of the eight listings states how long the animal has been waiting. Not one shelter page, not the Petfinder record, nowhere. &lt;code&gt;days_in_shelter&lt;/code&gt; is &lt;code&gt;"unknown"&lt;/code&gt; for all eight dogs.&lt;/p&gt;

&lt;p&gt;I could have estimated. It would have been easy and nobody would have checked. Instead &lt;code&gt;unknown&lt;/code&gt; became a real value in the mapping — valid input, contributes nothing — and a genuinely &lt;em&gt;missing&lt;/em&gt; value still throws, because "the shelter didn't say" and "I forgot to fill this in" must not look the same in a system that claims to derive things from records.&lt;/p&gt;

&lt;p&gt;The consequence is uncomfortable and I think it's the most honest thing on the page: &lt;strong&gt;not one of the eight dogs carries a weariness modifier in its own voice.&lt;/strong&gt; Weariness exists in exactly one place — Yuji's dial — where it is explicitly a demonstration of the function rather than a report about an animal.&lt;/p&gt;

&lt;p&gt;So the honest sentence is: &lt;em&gt;we can hear what waiting does to a voice; we cannot tell you how long any of these dogs has actually waited.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The absence &lt;strong&gt;is&lt;/strong&gt; the finding. The one number that would turn this from an interpretation into a measurement is the one number the sector does not publish. Yuji's listing says "way too long". That is as precise as the record ever gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The voices are an interpretation, not the dog.&lt;/strong&gt; Nobody at these shelters signed off on how their animals sound here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mapping is authored, not learned.&lt;/strong&gt; It encodes my reading of what a record implies about a voice. A different designer would write a different table and get different dogs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eight dogs is not a study.&lt;/strong&gt; Three shared sentences across five records is a real, checkable observation about eight listings, and nothing more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The degradation curve is a design choice.&lt;/strong&gt; The four weariness bands are chosen, not measured. No one has established what 120 days does to anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The waveform is decorative.&lt;/strong&gt; It's a progress bar drawn deterministically from the dog's id, not an analysis of the audio, and the page never claims otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;These dogs may already be adopted.&lt;/strong&gt; I hope so. Every card links to its source, captured 2026-08-16.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The API is not narrating this project — it's the visualisation layer. A documented pure function turns structured shelter data into a Voice Design prompt, and the generated voice &lt;em&gt;is&lt;/em&gt; the output of that function, shown with its working on every card. The same function called four times with one variable moved produces the hero's stay dial, so the "one variable" claim holds by construction rather than by assertion. Nineteen clips, one stock control voice named openly, thirty-two tests, no keys, no backend, no live calls.&lt;/p&gt;

&lt;p&gt;Built for the &lt;strong&gt;DEV Weekend Challenge: Dog Days Edition&lt;/strong&gt; — repo created 2026-08-16, deployed and written inside the window, with Claude Code as co-author.&lt;/p&gt;

&lt;p&gt;Eight dogs. One template. Two voices each. Go listen to Ponyboy, then click through to Hart and hear the same sentence again.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>elevenlabs</category>
      <category>dogdays</category>
    </item>
    <item>
      <title>ZeroToRepo: I Built an AI Agent That Turns Notion Ideas Into GitHub Repos Before Your Coffee Cools</title>
      <dc:creator>Abeera Lodhi</dc:creator>
      <pubDate>Sat, 28 Mar 2026 11:51:30 +0000</pubDate>
      <link>https://dev.to/abeera_lodhi/zerotorepo-i-built-an-ai-agent-that-turns-notion-ideas-into-github-repos-before-your-coffee-cools-1em2</link>
      <guid>https://dev.to/abeera_lodhi/zerotorepo-i-built-an-ai-agent-that-turns-notion-ideas-into-github-repos-before-your-coffee-cools-1em2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/notion-2026-03-04"&gt;Notion MCP Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;I have a Notion database with 30+ startup ideas. Every single one followed the same pattern: get excited, write down the idea, tell myself "I'll start tomorrow"… and then spend the next 3 hours setting up a repo, googling competitors, creating GitHub issues, and losing all momentum before writing a single line of real code.&lt;/p&gt;

&lt;p&gt;By idea #15, I realized the problem wasn't motivation. It was the &lt;strong&gt;Initialization Gap&lt;/strong&gt; that dead time between having an idea and pushing a first commit. So I built the thing that would have saved me all those wasted hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ZeroToRepo&lt;/strong&gt; is an autonomous AI agent. You check a single checkbox in Notion, and within 2 minutes, it has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Researched your market&lt;/strong&gt; : 5-8 live Brave Search queries, AI-analyzed to extract competitors, gaps, and trends 
‎ ‎ &lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Named your startup&lt;/strong&gt; : AI-generated brand name + tagline grounded in the research&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Written a Market Analysis&lt;/strong&gt; : saved as a formatted Notion sub-page with proper headings and bold text&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Built a 4-week strategy&lt;/strong&gt; : every task maps to a competitive gap: "Competitors lack X → Week 2: Build Y"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scaffolded a private GitHub repo&lt;/strong&gt; : README with competitor table, package.json, src/index.js, all via ghost commits (zero local git)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Opened labeled GitHub Issues&lt;/strong&gt; : each references the gap it addresses, with priority and owner&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Synthesized a Project Brief&lt;/strong&gt; : competitors, roadmap rationale, GitHub link, and timestamp, all back to Notion&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the thing that makes it actually interesting: &lt;strong&gt;the LLM decides what to do.&lt;/strong&gt; This isn't a hardcoded script that runs step 1, step 2, step 3. It's a Groq-powered agent with 12 tools and function calling. The AI sees the tool descriptions, picks what to call next, and if something fails, it decides how to recover. The AI is the orchestrator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two commands. That's it.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run setup   &lt;span class="c"&gt;# Interactive wizard: collects keys, tests all 4 connections&lt;/span&gt;
npm start       &lt;span class="c"&gt;# Watches Notion. Check ☑️ Trigger. Done.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/uzAfvpuQuM4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Show Us the Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Abeera81/zerotorepo" rel="noopener noreferrer"&gt;https://github.com/Abeera81/zerotorepo&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk3qi63wie19sr0yptvu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk3qi63wie19sr0yptvu.png" alt="ZeroToRepo system architecture showing Notion Database connecting via MCP to the ZeroToRepo Agent, which orchestrates Brave Search, Groq LLM, GitHub API, and Notion MCP Server" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The 4-Phase Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpga9n424ezge32szzhe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpga9n424ezge32szzhe.png" alt="ZeroToRepo 4-phase pipeline diagram showing Research, Strategy, Execution, and Synthesis phases with screenshots of the actual Notion and GitHub output" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent Tool Registry (12 Tools)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update_notion_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Update Notion status via MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deep_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Research&lt;/td&gt;
&lt;td&gt;5-8 Brave searches (with fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;analyze_market&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Research&lt;/td&gt;
&lt;td&gt;AI → competitors, gaps, insights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_startup_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Research&lt;/td&gt;
&lt;td&gt;Creative name + tagline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;save_market_analysis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Research&lt;/td&gt;
&lt;td&gt;Write to Notion via MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_strategy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strategy&lt;/td&gt;
&lt;td&gt;4-week gap-targeting roadmap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;save_strategy_to_notion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strategy&lt;/td&gt;
&lt;td&gt;Write to Notion via MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_github_repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Private repo + ghost commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set_github_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Store URL in Notion via MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_github_issues&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Labeled issues from roadmap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write_project_brief&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthesis&lt;/td&gt;
&lt;td&gt;Project Brief → Notion via MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;finalize_idea&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthesis&lt;/td&gt;
&lt;td&gt;Mark Done, uncheck trigger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;zerotorepo/&lt;/span&gt;
&lt;span class="s"&gt;├── src/&lt;/span&gt;
&lt;span class="s"&gt;│   ├── index.js&lt;/span&gt;            &lt;span class="c1"&gt;# CLI — polling loop, @clack/prompts TUI&lt;/span&gt;
&lt;span class="s"&gt;│   ├── agent.js&lt;/span&gt;            &lt;span class="c1"&gt;# 🤖 LLM agent — Groq function calling, 12 tools&lt;/span&gt;
&lt;span class="s"&gt;│   ├── stateMachine.js&lt;/span&gt;     &lt;span class="c1"&gt;# Routes: live → agent, mock → sequential&lt;/span&gt;
&lt;span class="s"&gt;│   ├── mcp-client.js&lt;/span&gt;       &lt;span class="c1"&gt;# MCP client — spawns Notion MCP server (stdio)&lt;/span&gt;
&lt;span class="s"&gt;│   ├── mcp-server.js&lt;/span&gt;       &lt;span class="c1"&gt;# ZeroToRepo AS MCP server — 7 tools for AI assistants&lt;/span&gt;
&lt;span class="s"&gt;│   ├── notion.js&lt;/span&gt;           &lt;span class="c1"&gt;# Notion via MCP — markdown-to-blocks converter&lt;/span&gt;
&lt;span class="s"&gt;│   ├── research.js&lt;/span&gt;         &lt;span class="c1"&gt;# Brave Search (5-8 queries) + Groq analysis&lt;/span&gt;
&lt;span class="s"&gt;│   ├── scaffold.js&lt;/span&gt;         &lt;span class="c1"&gt;# GitHub ghost commits + rich README&lt;/span&gt;
&lt;span class="s"&gt;│   ├── roadmap.js&lt;/span&gt;          &lt;span class="c1"&gt;# 4-week gap-targeting strategy&lt;/span&gt;
&lt;span class="s"&gt;│   └── brief.js&lt;/span&gt;            &lt;span class="c1"&gt;# Project Brief synthesis&lt;/span&gt;
&lt;span class="s"&gt;├── scripts/&lt;/span&gt;
&lt;span class="s"&gt;│   ├── setup.js&lt;/span&gt;            &lt;span class="c1"&gt;# Interactive setup wizard&lt;/span&gt;
&lt;span class="s"&gt;│   └── reset-db.js&lt;/span&gt;         &lt;span class="c1"&gt;# Reset Notion database&lt;/span&gt;
&lt;span class="s"&gt;└── prompts/&lt;/span&gt;                &lt;span class="c1"&gt;# LLM system prompts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How I Used Notion MCP
&lt;/h2&gt;

&lt;p&gt;Notion MCP is the &lt;strong&gt;central nervous system&lt;/strong&gt; of ZeroToRepo. Every single Notion operation goes through MCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consuming Notion MCP Server As MCP Client
&lt;/h3&gt;

&lt;p&gt;ZeroToRepo spawns &lt;code&gt;@notionhq/notion-mcp-server&lt;/code&gt; (v2.2.1) as a child process via stdio transport. All 22 MCP tools are available. We use 5:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MCP Tool&lt;/th&gt;
&lt;th&gt;Where We Use It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API-query-data-source&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Poll for triggered ideas every 5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API-patch-page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update status (Researching → Planning → Building → Done)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API-post-page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create Market Analysis, Strategy &amp;amp; Roadmap, Project Brief sub-pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API-get-block-children&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Idempotency checks — skip if sub-page already exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API-delete-a-block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Database reset script&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The MCP client handles connection lifecycle (lazy connect on first call, graceful disconnect on SIGINT/SIGTERM) and transparent JSON parsing of tool results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's a problem I didn't expect:&lt;/strong&gt; Notion doesn't render markdown. When you write research back to Notion, raw markdown appears as ugly plain text with literal &lt;code&gt;**asterisks**&lt;/code&gt; everywhere. So I built a &lt;code&gt;markdownToNotionBlocks()&lt;/code&gt; converter that transforms markdown into native Notion block types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Heading     → heading_1&lt;/span&gt;
&lt;span class="gu"&gt;## Subheading  → heading_2&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Bullet       → bulleted_list_item
&lt;span class="p"&gt;1.&lt;/span&gt; Number      → numbered_list_item
&lt;span class="gt"&gt;&amp;gt; Quote        → quote&lt;/span&gt;
---            → divider
&lt;span class="gs"&gt;**bold**&lt;/span&gt;       → rich_text with bold annotation
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;link&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    → rich_text with link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every sub-page (Market Analysis, Strategy &amp;amp; Roadmap, Project Brief) renders with proper headings, bold text, bullet lists, and links, just like hand-crafted Notion pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposing ZeroToRepo As an MCP Server
&lt;/h3&gt;

&lt;p&gt;Here's where it gets interesting. ZeroToRepo isn't just an MCP &lt;em&gt;client&lt;/em&gt;... it's also an MCP &lt;em&gt;server&lt;/em&gt;. Any MCP-compatible AI assistant (Claude Desktop, Cursor, VS Code) can call ZeroToRepo's 7 tools directly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;process_idea&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run the full 4-phase pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research_competitors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deep competitive research only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creative startup name from research&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scaffold_repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create GitHub repo with scaffold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_roadmap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generate &amp;amp; create roadmap issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_brief&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthesize project brief&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_notion_ideas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all ideas from Notion database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This means Claude can say: &lt;em&gt;"Hey ZeroToRepo, research 'AI pet care' and scaffold a repo"&lt;/em&gt; and it just works. The setup wizard even auto-generates the MCP config for VS Code or Claude Desktop so you don't have to write JSON by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Full MCP Data Flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrdkh9qnaqqes3mhdsml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrdkh9qnaqqes3mhdsml.png" alt="ZeroToRepo MCP data flow from Notion trigger through the 14-step agent pipeline to final Notion output" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total MCP calls per pipeline run:&lt;/strong&gt; ~12-15 (status updates, sub-page creation, URL setting, trigger reset)&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes This Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LLM-Driven Orchestration (Not Hardcoded)
&lt;/h3&gt;

&lt;p&gt;Most hackathon projects chain API calls in a fixed sequence. ZeroToRepo's agent uses Groq function calling. The LLM receives 12 tool descriptions and decides which to call next. If &lt;code&gt;deep_search&lt;/code&gt; fails, it sees the error and can retry with a different query or skip to &lt;code&gt;analyze_market&lt;/code&gt; with fallback data. This is genuine AI agent behavior, not a script.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dual MCP Architecture
&lt;/h3&gt;

&lt;p&gt;We're not just &lt;em&gt;consuming&lt;/em&gt; Notion MCP, we're also &lt;em&gt;exposing&lt;/em&gt; ZeroToRepo as an MCP server. This creates a composable tool ecosystem where AI assistants can chain ZeroToRepo with other MCP servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gap-Targeted Strategy
&lt;/h3&gt;

&lt;p&gt;The roadmap isn't generic. Every task explicitly references a competitive gap: &lt;em&gt;"Competitors lack real-time sync → Week 1: Build WebSocket infrastructure."&lt;/em&gt; Phase 2 reads Phase 1's gap analysis and generates tasks that directly target those gaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ghost Commits (Zero Local Git)
&lt;/h3&gt;

&lt;p&gt;Repos are created entirely via GitHub's Git Data API, blobs, trees, commits, refs, all remotely. No &lt;code&gt;git clone&lt;/code&gt;, no local filesystem, no SSH keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  $0 Total Cost
&lt;/h3&gt;

&lt;p&gt;Groq free tier (100k tokens/day), Brave Search free tier (2k queries/month), GitHub API (free with PAT), Notion API (free integration). A full pipeline run uses ~20k tokens, you get ~5 runs per day for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js v20+&lt;/td&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MCP client + server framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@notionhq/notion-mcp-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Notion MCP integration (22 tools)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groq API (Llama-3.3-70B)&lt;/td&gt;
&lt;td&gt;LLM function calling, analysis, generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brave Search API&lt;/td&gt;
&lt;td&gt;Real-time competitive intelligence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@octokit/rest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GitHub repo creation, ghost commits, issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@clack/prompts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Beautiful interactive CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
