<?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: Byunghee Kim</title>
    <description>The latest articles on DEV Community by Byunghee Kim (@tacckim).</description>
    <link>https://dev.to/tacckim</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%2F4093342%2Fb47d7c98-7863-41e4-9c6c-1ed304d817af.png</url>
      <title>DEV Community: Byunghee Kim</title>
      <link>https://dev.to/tacckim</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tacckim"/>
    <language>en</language>
    <item>
      <title>A year of coding by talking: what I gained and what I lost</title>
      <dc:creator>Byunghee Kim</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:35:29 +0000</pubDate>
      <link>https://dev.to/tacckim/a-year-of-coding-by-talking-what-i-gained-and-what-i-lost-1p15</link>
      <guid>https://dev.to/tacckim/a-year-of-coding-by-talking-what-i-gained-and-what-i-lost-1p15</guid>
      <description>&lt;p&gt;2024 was the year AI was everywhere. The ads, the praise, the feeling that something had already been decided without me.&lt;/p&gt;

&lt;p&gt;I am in my fifties. I had to decide whether to watch or take part.&lt;/p&gt;

&lt;p&gt;I decided to take part. I started with VS Code, on a paid plan.&lt;/p&gt;

&lt;p&gt;I did not have to think about what to build. Something had been sitting in my head for years: an automated trading system.&lt;/p&gt;

&lt;p&gt;I have lived fifty years, and while raising children the money got tighter, not looser. Financial freedom was moving away from me, not toward me. So I wanted to make money with automated trading. I think the idea first arrived in my mid-forties. That is why the decision to take part came so quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whatever I said out loud would simply get built.&lt;/strong&gt; That was the hope I walked in on.&lt;/p&gt;

&lt;p&gt;I do not really know how to code. But the AI would handle that part, so I trusted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the illusion first cracked
&lt;/h2&gt;

&lt;p&gt;A year inside VS Code taught me that two names mattered: GPT and Claude. I used them in turn. I used them one at a time.&lt;/p&gt;

&lt;p&gt;Two problems.&lt;/p&gt;

&lt;p&gt;First, even on a paid plan the usage ran out fast. Faster than I expected. The road ahead was long and I was sitting still, waiting for a quota to reset.&lt;/p&gt;

&lt;p&gt;The second one was worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The explanations were excellent. The results were not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is where the illusion cracked for the first time.&lt;/p&gt;

&lt;p&gt;I still could not let go, so I paid for more. Adding Cursor bought me some headroom. And a different problem showed up immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switch the model and it wants to start over
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Change the model, and it wants to rewrite everything from the beginning.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Handed code written by a different AI, it would rather replace the whole thing than edit it. That is when I understood that switching AI mid-project is a bad idea.&lt;/p&gt;

&lt;p&gt;Everyone talks about pricing. Almost nobody talks about this one. And this is the one that actually held me back.&lt;/p&gt;

&lt;p&gt;I spent a lot of time fighting the tool. In the end I paid for Claude's hundred-dollar plan, and from then on I worked with Claude.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I gained: the job nobody wanted
&lt;/h2&gt;

&lt;p&gt;I worked on trading at home, whenever I had time. Then at work I ran into a repetitive data-entry task.&lt;/p&gt;

&lt;p&gt;Other people usually handled it, and all of them found it painful. When something got entered wrong, the stress was real. I took my turn at it too, and I hated it.&lt;/p&gt;

&lt;p&gt;That is when Claude came to mind.&lt;/p&gt;

&lt;p&gt;Not really believing it would work, I said I would try to automate it, and started building.&lt;/p&gt;

&lt;p&gt;This did not go smoothly either.&lt;/p&gt;

&lt;p&gt;As the code got longer, it kept showing me that &lt;strong&gt;it no longer knew what it had done.&lt;/strong&gt; So I decided to split the files, and asked Claude to split them. That finished quickly.&lt;/p&gt;

&lt;p&gt;That is where the trouble started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting the files made it worse
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It started working without knowing what was in which file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same change would land in several files at once. It sprawled. I told it to check the files. Every new session it fell apart again.&lt;/p&gt;

&lt;p&gt;It kept happening, so &lt;strong&gt;I merged everything back into one file.&lt;/strong&gt; I did that out of pure frustration. By then the code was five thousand lines.&lt;/p&gt;

&lt;p&gt;Writing documentation did not occur to me at that point. Merging was the only move I had.&lt;/p&gt;

&lt;p&gt;The automation worked in the end.&lt;/p&gt;

&lt;p&gt;What was left behind was a single file of more than ten thousand lines. I would not recommend it to anyone. It was simply the best I could do at the time.&lt;/p&gt;

&lt;p&gt;That was where I felt the limits again, and started looking for another way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maybe the AI is the one that needs the standards.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That thought started here.&lt;/p&gt;

&lt;p&gt;The whole thing took about two months. It felt like a small success inside a beginner's failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not get: the trading system
&lt;/h2&gt;

&lt;p&gt;I still do not have it.&lt;/p&gt;

&lt;p&gt;Every time I thought it was close, something came back to tell me it was not.&lt;/p&gt;

&lt;p&gt;The bigger problem was somewhere else. &lt;strong&gt;There were many tests. There was a great deal of verification. All of it stood alone. None of it was ever brought together.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude would tell me it was all there. What was there were test results. &lt;strong&gt;There was no integrated file that actually traded.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That one was heavy. I still could not let go, because the dream of it keeps circling in my head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both stories turned out to be the same story
&lt;/h2&gt;

&lt;p&gt;I only saw it while writing this.&lt;/p&gt;

&lt;p&gt;What I built and what I failed to build had &lt;strong&gt;the same cause.&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;&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;th&gt;what actually happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the data-entry automation&lt;/td&gt;
&lt;td&gt;worked&lt;/td&gt;
&lt;td&gt;I &lt;strong&gt;put the scattered pieces back together&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the trading system&lt;/td&gt;
&lt;td&gt;did not&lt;/td&gt;
&lt;td&gt;I &lt;strong&gt;never managed to put them together&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same illness, twice. One of them I could reverse. The other I could not.&lt;/p&gt;

&lt;p&gt;So this is where I have landed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI needs standards, and it needs records.&lt;/strong&gt; Not because it is bad at its job, but because its limits are clear enough that I should be the one preparing for them. That is a statement about what I have to do, not a complaint about the tool.&lt;/p&gt;

&lt;p&gt;This series is the record of that year. It is not a success story. It is an account of what the AI was looking at, what I was looking at, and why the two kept failing to line up.&lt;/p&gt;

&lt;p&gt;Will this ever work?&lt;/p&gt;

&lt;p&gt;I still do not know.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Unread badges that would not clear: 59 of my 64 messages were junk</title>
      <dc:creator>Byunghee Kim</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:29:16 +0000</pubDate>
      <link>https://dev.to/tacckim/unread-badges-that-would-not-clear-59-of-my-64-messages-were-junk-21c5</link>
      <guid>https://dev.to/tacckim/unread-badges-that-would-not-clear-59-of-my-64-messages-were-junk-21c5</guid>
      <description>&lt;p&gt;I am building a small chat feature for an HR system. Shared hosting, PHP, no WebSocket, polling every few seconds. Nothing exotic.&lt;/p&gt;

&lt;p&gt;Then a bug report landed. This is what I actually wrote, translated from Korean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If I open room A, it gets read. So if I close it and open another room, it should stay read. But that doesn't happen — the badge on the room I already opened is still there."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The original had two typos in it. That tells you how many times I had already retyped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Open room A. Badge clears.&lt;/li&gt;
&lt;li&gt;Open room B. &lt;strong&gt;Room A has a badge again.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Nobody sent anything. I was the only person on the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line is what made it worth writing about. Unread messages were appearing in rooms that nobody was in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong theory #1: the read-state cookie
&lt;/h2&gt;

&lt;p&gt;The engine stores "which messages you have already seen" client side. My first guess was an encoding problem — the value round-tripping badly, so the comparison silently failed.&lt;/p&gt;

&lt;p&gt;I checked. The cookie was fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong theory #2: a stale high-water mark
&lt;/h2&gt;

&lt;p&gt;Second guess: the "last message ID I have seen" was being captured before the room finished loading, so it lagged one step behind. That would explain a badge reappearing exactly one room-switch later, which is what it looked like.&lt;/p&gt;

&lt;p&gt;Also wrong.&lt;/p&gt;

&lt;p&gt;Two theories, both plausible, both about &lt;strong&gt;why the counter was reading the data wrong&lt;/strong&gt;. Neither of them questioned the data.&lt;/p&gt;

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

&lt;p&gt;I stopped guessing and counted rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&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="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;chat_messages&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/channelEnter    31
/channelLeave    28
real messages     5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;64 rows in the table. &lt;strong&gt;59 of them were not written by a human.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;The chat engine writes a system message every time you enter or leave a room. There is a config flag that is supposed to turn this off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'showChannelMessages'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name is exactly right, and that is the trap. It controls &lt;strong&gt;showing&lt;/strong&gt;. It does not control &lt;strong&gt;storing&lt;/strong&gt;. The rows are still inserted, still owned by a channel, still newer than your last-seen marker — so the unread counter counts them.&lt;/p&gt;

&lt;p&gt;Which produces this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open room A  -&amp;gt;  system stores "entered" in room A
Open room B  -&amp;gt;  system stores "left"    in room A
             -&amp;gt;  room A now has 2 unread messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The act of switching rooms was generating the unread messages.&lt;/strong&gt; I was the one creating them, by clicking around looking for the bug.&lt;/p&gt;

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

&lt;p&gt;The engine is a third-party library and I did not want to patch it, so I overrode one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;insertChatBotMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$channelID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$messageText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ip&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="nv"&gt;$mode&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'showChannelMessages'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$messageText&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="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/channelEnter'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/channelLeave'&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;;&lt;/span&gt;                          &lt;span class="c1"&gt;// not shown -&amp;gt; not stored&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="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;insertChatBotMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$channelID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$messageText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$mode&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. The rule it encodes is one sentence: &lt;strong&gt;if we are never going to display it, do not write it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two follow-ups were needed, and I would have missed both if I had stopped at the first one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exclude system rows from the unread count&lt;/strong&gt; — the junk already in the table does not delete itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclude them from search too.&lt;/strong&gt; Otherwise searching for a username returns a wall of &lt;code&gt;/channelEnter username&lt;/code&gt;. Same rows, second symptom, different screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The effect
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unread per room&lt;/td&gt;
&lt;td&gt;16 · 5 · 8 · 3 · 6 · 6 · 4 · 2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 · 1 · 0 · 0 · 0 · 0 · 0 · 0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;junk share of table&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0 new rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row mattered more than the first. On shared hosting the messages table is the only thing that grows without a ceiling, and I had been sizing a weekly archive job around it. I was designing storage policy for a table that was 92% garbage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A flag named after the UI can quietly own the database.&lt;/strong&gt; &lt;code&gt;showChannelMessages&lt;/code&gt; reads as a display setting and behaves as one — and still writes rows forever. If a boolean controls both display and persistence, the name will only ever describe one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When a counter is wrong, count the rows before theorizing.&lt;/strong&gt; My two wrong theories were both about the reading side, because that is where the visible code was. The query took thirty seconds and ended the argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A bug that only shows up when you go looking for it is not rare.&lt;/strong&gt; Switching rooms was both the diagnostic action and the cause. That is worth remembering the next time something only reproduces while you are watching.&lt;/p&gt;

</description>
      <category>php</category>
      <category>debugging</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>Unread badges that would not clear: 59 of my 64 messages were junk</title>
      <dc:creator>Byunghee Kim</dc:creator>
      <pubDate>Tue, 25 Aug 2026 06:11:22 +0000</pubDate>
      <link>https://dev.to/tacckim/unread-badges-that-would-not-clear-59-of-my-64-messages-were-junk-4fi1</link>
      <guid>https://dev.to/tacckim/unread-badges-that-would-not-clear-59-of-my-64-messages-were-junk-4fi1</guid>
      <description>&lt;p&gt;I am building a small chat feature for an HR system. Shared hosting, PHP, no WebSocket, polling every few seconds. Nothing exotic.&lt;/p&gt;

&lt;p&gt;Then a bug report landed. This is what I actually wrote, translated from Korean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If I open room A, it gets read. So if I close it and open another room, it should stay read. But that doesn't happen — the badge on the room I already opened is still there."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The original had two typos in it. That tells you how many times I had already retyped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Open room A. Badge clears.&lt;/li&gt;
&lt;li&gt;Open room B. &lt;strong&gt;Room A has a badge again.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Nobody sent anything. I was the only person on the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line is what made it worth writing about. Unread messages were appearing in rooms that nobody was in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong theory #1: the read-state cookie
&lt;/h2&gt;

&lt;p&gt;The engine stores "which messages you have already seen" client side. My first guess was an encoding problem — the value round-tripping badly, so the comparison silently failed.&lt;/p&gt;

&lt;p&gt;I checked. The cookie was fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong theory #2: a stale high-water mark
&lt;/h2&gt;

&lt;p&gt;Second guess: the "last message ID I have seen" was being captured before the room finished loading, so it lagged one step behind. That would explain a badge reappearing exactly one room-switch later, which is what it looked like.&lt;/p&gt;

&lt;p&gt;Also wrong.&lt;/p&gt;

&lt;p&gt;Two theories, both plausible, both about &lt;strong&gt;why the counter was reading the data wrong&lt;/strong&gt;. Neither of them questioned the data.&lt;/p&gt;

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

&lt;p&gt;I stopped guessing and counted rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&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="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;chat_messages&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/channelEnter    31
/channelLeave    28
real messages     5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;64 rows in the table. &lt;strong&gt;59 of them were not written by a human.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;The chat engine writes a system message every time you enter or leave a room. There is a config flag that is supposed to turn this off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'showChannelMessages'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name is exactly right, and that is the trap. It controls &lt;strong&gt;showing&lt;/strong&gt;. It does not control &lt;strong&gt;storing&lt;/strong&gt;. The rows are still inserted, still owned by a channel, still newer than your last-seen marker — so the unread counter counts them.&lt;/p&gt;

&lt;p&gt;Which produces this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open room A  -&amp;gt;  system stores "entered" in room A
Open room B  -&amp;gt;  system stores "left"    in room A
             -&amp;gt;  room A now has 2 unread messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The act of switching rooms was generating the unread messages.&lt;/strong&gt; I was the one creating them, by clicking around looking for the bug.&lt;/p&gt;

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

&lt;p&gt;The engine is a third-party library and I did not want to patch it, so I overrode one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;insertChatBotMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$channelID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$messageText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ip&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="nv"&gt;$mode&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'showChannelMessages'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$messageText&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="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/channelEnter'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/channelLeave'&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;;&lt;/span&gt;                          &lt;span class="c1"&gt;// not shown -&amp;gt; not stored&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="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;insertChatBotMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$channelID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$messageText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$mode&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. The rule it encodes is one sentence: &lt;strong&gt;if we are never going to display it, do not write it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two follow-ups were needed, and I would have missed both if I had stopped at the first one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exclude system rows from the unread count&lt;/strong&gt; — the junk already in the table does not delete itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclude them from search too.&lt;/strong&gt; Otherwise searching for a username returns a wall of &lt;code&gt;/channelEnter username&lt;/code&gt;. Same rows, second symptom, different screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The effect
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unread per room&lt;/td&gt;
&lt;td&gt;16 · 5 · 8 · 3 · 6 · 6 · 4 · 2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 · 1 · 0 · 0 · 0 · 0 · 0 · 0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;junk share of table&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0 new rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row mattered more than the first. On shared hosting the messages table is the only thing that grows without a ceiling, and I had been sizing a weekly archive job around it. I was designing storage policy for a table that was 92% garbage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A flag named after the UI can quietly own the database.&lt;/strong&gt; &lt;code&gt;showChannelMessages&lt;/code&gt; reads as a display setting and behaves as one — and still writes rows forever. If a boolean controls both display and persistence, the name will only ever describe one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When a counter is wrong, count the rows before theorizing.&lt;/strong&gt; My two wrong theories were both about the reading side, because that is where the visible code was. The query took thirty seconds and ended the argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A bug that only shows up when you go looking for it is not rare.&lt;/strong&gt; Switching rooms was both the diagnostic action and the cause. That is worth remembering the next time something only reproduces while you are watching.&lt;/p&gt;

</description>
      <category>php</category>
      <category>debugging</category>
      <category>webdev</category>
      <category>database</category>
    </item>
  </channel>
</rss>
