<?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: Vignan Nallani</title>
    <description>The latest articles on DEV Community by Vignan Nallani (@vignannallani).</description>
    <link>https://dev.to/vignannallani</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%2F4003483%2F59e5e8f6-fe79-4f0b-ba99-073499fc8054.png</url>
      <title>DEV Community: Vignan Nallani</title>
      <link>https://dev.to/vignannallani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vignannallani"/>
    <language>en</language>
    <item>
      <title>I gave an AI a memory of my interview history — here's what I learned building on Cognee</title>
      <dc:creator>Vignan Nallani</dc:creator>
      <pubDate>Tue, 30 Jun 2026 07:08:26 +0000</pubDate>
      <link>https://dev.to/vignannallani/i-gave-an-ai-a-memory-of-my-interview-history-heres-what-i-learned-building-on-cognee-5h76</link>
      <guid>https://dev.to/vignannallani/i-gave-an-ai-a-memory-of-my-interview-history-heres-what-i-learned-building-on-cognee-5h76</guid>
      <description>&lt;p&gt;I gave an AI a memory of my interview history — here's what I learned building on Cognee&lt;/p&gt;

&lt;p&gt;Every time I've used an AI to prep for interviews, it forgets me the second I close the tab. I paste my resume, it gives me the same advice it gives everyone, and the next session starts from zero. For something that's supposed to help me improve over &lt;em&gt;time&lt;/em&gt;, that amnesia is the whole problem.&lt;/p&gt;

&lt;p&gt;So for the &lt;strong&gt;Hangover Part AI: Where's My Context?&lt;/strong&gt; hackathon, I built &lt;strong&gt;Interview Memory Coach&lt;/strong&gt; — an AI coach that builds a permanent, structured memory of your resume, your target role, and every past interview where you struggled, then coaches you against your &lt;em&gt;actual&lt;/em&gt; gaps instead of generic tips. It runs on &lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;Cognee&lt;/a&gt;, an open-source memory layer for AI agents.&lt;/p&gt;

&lt;p&gt;This is the story of building it: what worked, the bugs that didn't, and what I learned about giving an AI a memory that actually persists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: graph-grounded, not just retrieved
&lt;/h2&gt;

&lt;p&gt;Most "AI with memory" projects are really just RAG — you embed some text, retrieve the nearest chunks, and hope the LLM stitches them into something coherent. That works until the answer depends on &lt;em&gt;relationships&lt;/em&gt; between facts rather than the facts themselves.&lt;/p&gt;

&lt;p&gt;Interview coaching is exactly that kind of problem. "How should I prepare for system design?" shouldn't return textbook advice — it should connect &lt;em&gt;this candidate&lt;/em&gt; to &lt;em&gt;their past failures&lt;/em&gt; to &lt;em&gt;the role they're targeting&lt;/em&gt;. That's a graph traversal, not a similarity search.&lt;/p&gt;

&lt;p&gt;Cognee's &lt;code&gt;GRAPH_COMPLETION&lt;/code&gt; search does this in one call: it walks a knowledge graph built from your data and synthesizes an answer grounded in connected facts. When my test candidate Jane asks about system design, the coach doesn't recite fundamentals — it says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"You've struggled with system design in past interviews — you were asked to design a URL shortener at 10M req/day, proposed SQLite, and admitted you couldn't handle distributed scale. The senior role you're targeting requires exactly this."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That specificity is the entire point. And it only works because Cognee turned her resume and interview notes into a graph where &lt;code&gt;Jane → system design → distributed databases → url shortener&lt;/code&gt; are connected nodes, not loose text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the full memory lifecycle
&lt;/h2&gt;

&lt;p&gt;Cognee's memory isn't just store-and-retrieve. It has four operations — &lt;strong&gt;remember, recall, improve, forget&lt;/strong&gt; — and I wanted to use all of them meaningfully, not just check boxes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;remember&lt;/strong&gt; — ingest the resume, job description, and past Q&amp;amp;A; &lt;code&gt;cognify&lt;/code&gt; extracts the graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recall&lt;/strong&gt; — &lt;code&gt;GRAPH_COMPLETION&lt;/code&gt; traverses the graph to answer a coaching question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;improve&lt;/strong&gt; — an enrichment pass (&lt;code&gt;memify&lt;/code&gt;) that deepens the stored graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;forget&lt;/strong&gt; — wipe stale interview history so it stops polluting coaching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting each of these to behave for a fast, free-tier, single-user demo is where the real engineering happened. Here are the three problems that taught me the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the 8B model that couldn't follow a schema
&lt;/h2&gt;

&lt;p&gt;To keep costs at zero, I started with a two-model setup: a small, fast model (&lt;code&gt;llama-3.1-8b-instant&lt;/code&gt; via Groq) for graph extraction, and a larger one (&lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt;) for the actual coaching answers. Cheap extraction, quality answers — sensible split.&lt;/p&gt;

&lt;p&gt;Except ingest kept crashing. The error trace showed the 8B model was producing &lt;code&gt;KnowledgeGraph&lt;/code&gt; JSON that violated Cognee's schema — nodes missing their required &lt;code&gt;description&lt;/code&gt; field. Cognee retried, the retries also came back malformed, and the whole pipeline died.&lt;/p&gt;

&lt;p&gt;The fix taught me something about matching model size to task constraints: strict structured output is &lt;em&gt;hard&lt;/em&gt; for small models, and no amount of retrying fixes a model that can't reliably hit a schema. Since ingest only runs once (the sample pre-loads at startup), I moved extraction to the 70B model too. The cost is trivial for a one-time call, the crashes vanished — and as a bonus, the richer model produced a &lt;em&gt;denser&lt;/em&gt; graph (12 nodes became 20). Sometimes the "expensive" choice is the right one once you realize how rarely the call actually fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: the coach that wouldn't answer twice
&lt;/h2&gt;

&lt;p&gt;Early on, asking the same question twice produced a bizarre result: the second time, instead of coaching, the app replied &lt;em&gt;"I'll wait for further clarification."&lt;/em&gt; It had decided it already answered.&lt;/p&gt;

&lt;p&gt;This one took some digging. Cognee's &lt;code&gt;search()&lt;/code&gt; has a &lt;code&gt;session_id&lt;/code&gt; parameter that defaults to &lt;code&gt;'default_session'&lt;/code&gt; — so every question I asked landed in the &lt;em&gt;same&lt;/em&gt; conversation session. Cognee, reasonably, treated my repeated question as a continuation of an ongoing chat and short-circuited rather than re-answering.&lt;/p&gt;

&lt;p&gt;The fix was to pass a fresh &lt;code&gt;uuid.uuid4()&lt;/code&gt; as the &lt;code&gt;session_id&lt;/code&gt; on every coaching call, plus setting &lt;code&gt;CACHING=false&lt;/code&gt;. Now each question is an independent, stateless query — exactly what a coaching tool wants. The lesson: a default that's perfect for a multi-turn agent (session continuity) is wrong for a stateless Q&amp;amp;A tool, and knowing &lt;em&gt;why&lt;/em&gt; the default exists made the fix obvious instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: the forget() that didn't forget
&lt;/h2&gt;

&lt;p&gt;This was the sneakiest. I'd call &lt;code&gt;cognee.forget(dataset="session")&lt;/code&gt;, it would return success — and then recall would &lt;em&gt;still answer&lt;/em&gt; with the supposedly-forgotten data. A silent failure: the kind that looks fine in a demo right up until a judge clicks the button and watches it lie.&lt;/p&gt;

&lt;p&gt;The root cause was a genuinely subtle interaction. For single-user local mode, you disable access control (&lt;code&gt;ENABLE_BACKEND_ACCESS_CONTROL=false&lt;/code&gt;). But with access control off, &lt;code&gt;GRAPH_COMPLETION&lt;/code&gt; becomes a &lt;em&gt;global&lt;/em&gt; graph traversal — it doesn't filter by dataset. So deleting one dataset's nodes left residual nodes that the global search still happily found.&lt;/p&gt;

&lt;p&gt;For a single-dataset demo, the reliable answer was a full &lt;code&gt;prune_system&lt;/code&gt; wipe — which, since the app only ever holds one session, is functionally equivalent to forgetting that session. (True multi-dataset surgical forget would mean enabling access control and scoping searches per user — the production path, which I noted but didn't need.) What I appreciated here is that Cognee gave me enough visibility into its own internals to actually &lt;em&gt;find&lt;/em&gt; this, rather than just shrugging at a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I think about Cognee after building on it
&lt;/h2&gt;

&lt;p&gt;Honestly? It's a genuinely powerful piece of infrastructure, and the graph-vector hybrid is the real deal — being able to traverse a knowledge graph &lt;em&gt;and&lt;/em&gt; fall back to semantic search, in one call, is exactly what "AI memory" should mean. The lifecycle API (remember/recall/improve/forget) is a clean mental model for something that's usually a tangle.&lt;/p&gt;

&lt;p&gt;The rough edges I hit — the schema strictness, the session defaults, the dataset-scoping behavior — weren't bugs so much as &lt;em&gt;behaviors I had to understand&lt;/em&gt;. And the fact that I &lt;em&gt;could&lt;/em&gt; understand them (open source, readable internals, real error messages) is the difference between a tool you fight and a tool you learn. I'd reach for it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The build is fully self-hosted — SQLite, LanceDB, an embedded graph store, local CPU embeddings, and Groq's free tier for the LLM. No data leaves the machine, and it costs nothing to run.&lt;/p&gt;

&lt;p&gt;But the part I'm keeping isn't the stack — it's the reframe. "AI memory" isn't a vector store with extra steps. It's a &lt;em&gt;graph&lt;/em&gt; of what the system knows about you, that can grow, deepen, and be deliberately forgotten. Once interview prep remembers your actual history, it stops giving you everyone's advice and starts giving you &lt;em&gt;yours&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Code's here: &lt;a href="https://github.com/VignanNallani/interview-memory-coach" rel="noopener noreferrer"&gt;github.com/VignanNallani/interview-memory-coach&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for the Hangover Part AI hackathon. This project was built with AI coding assistants for implementation; all architecture and debugging decisions were mine, and I can defend every one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cognee</category>
      <category>ai</category>
      <category>python</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>An infinite loop hiding in a string search: debugging objectIsQueried in KubeStellar</title>
      <dc:creator>Vignan Nallani</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:27:15 +0000</pubDate>
      <link>https://dev.to/vignannallani/an-infinite-loop-hiding-in-a-string-search-debugging-objectisqueried-in-kubestellar-2d29</link>
      <guid>https://dev.to/vignannallani/an-infinite-loop-hiding-in-a-string-search-debugging-objectisqueried-in-kubestellar-2d29</guid>
      <description>&lt;p&gt;Some bugs crash loudly. The more interesting ones just &lt;em&gt;hang&lt;/em&gt; — no panic, no error, no stack trace, just a goroutine quietly spinning forever on input that looks completely ordinary. I ran into one of those recently while reading through &lt;a href="https://github.com/kubestellar/kubestellar" rel="noopener noreferrer"&gt;KubeStellar&lt;/a&gt;, a CNCF Sandbox project for multi-cluster configuration management. Here's the hunt, the root cause, and the one-character-deep mistake that caused it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the function was supposed to do
&lt;/h2&gt;

&lt;p&gt;Deep in KubeStellar's status-combination code (&lt;code&gt;pkg/status/combinedstatus-resolution.go&lt;/code&gt;) there's a small helper called &lt;code&gt;objectIsQueried&lt;/code&gt;. Its job is simple: given a query string and an object name, decide whether the object appears in the query &lt;em&gt;as a whole word&lt;/em&gt; — not as a fragment buried inside a longer identifier. So &lt;code&gt;"foo"&lt;/code&gt; should match &lt;code&gt;"select foo from bar"&lt;/code&gt; but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;"foobar"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To do that, it walks the string looking for each occurrence of the object, and for each one asks a second helper, &lt;code&gt;isWholeWord&lt;/code&gt;, whether that position is bounded by non-alphanumeric characters. Reasonable design. Here's the original loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;objectIsQueried&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// search from idx onward&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isWholeWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&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="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&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;Read it quickly and it looks fine. That's exactly what made it dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: relative vs. absolute indices
&lt;/h2&gt;

&lt;p&gt;The bug lives entirely in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;obj&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;strings.Index&lt;/code&gt; returns the offset of &lt;code&gt;obj&lt;/code&gt; &lt;strong&gt;relative to the slice it was given&lt;/strong&gt; — and the slice here is &lt;code&gt;(*query)[idx:]&lt;/code&gt;, not the whole string. But the code then turns around and uses that relative offset as if it were an absolute position in the original &lt;code&gt;query&lt;/code&gt;, both when calling &lt;code&gt;isWholeWord(query, idx, ...)&lt;/code&gt; and when advancing &lt;code&gt;idx += len(obj)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As long as the first match happens to sit at the start of the string, relative and absolute agree, and everything works. The moment the search slice starts somewhere in the middle, the two diverge — and the loop starts using a wrong index that points back into territory it already searched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching it spin
&lt;/h2&gt;

&lt;p&gt;The clearest way to see the failure is to trace the input &lt;code&gt;query = "foobar foo"&lt;/code&gt;, &lt;code&gt;obj = "foo"&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;idx = 0.&lt;/strong&gt; &lt;code&gt;strings.Index("foobar foo", "foo")&lt;/code&gt; → &lt;code&gt;0&lt;/code&gt;. &lt;code&gt;isWholeWord&lt;/code&gt; at position 0? The character after &lt;code&gt;"foo"&lt;/code&gt; is &lt;code&gt;b&lt;/code&gt; — alphanumeric — so it's embedded in &lt;code&gt;"foobar"&lt;/code&gt;, not a whole word. &lt;code&gt;false&lt;/code&gt;. Advance: &lt;code&gt;idx += 3&lt;/code&gt; → &lt;strong&gt;idx = 3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;idx = 3.&lt;/strong&gt; Now we search the slice &lt;code&gt;"bar foo"&lt;/code&gt;. &lt;code&gt;strings.Index("bar foo", "foo")&lt;/code&gt; → &lt;code&gt;4&lt;/code&gt; &lt;em&gt;relative to that slice&lt;/em&gt;. The code stores &lt;code&gt;4&lt;/code&gt; as if it were absolute. &lt;code&gt;isWholeWord&lt;/code&gt; at absolute position 4 is wrong, returns &lt;code&gt;false&lt;/code&gt;. Advance: &lt;code&gt;idx += 3&lt;/code&gt; → &lt;strong&gt;idx = 7&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;idx = 7.&lt;/strong&gt; Search the slice &lt;code&gt;"foo"&lt;/code&gt;. &lt;code&gt;strings.Index("foo", "foo")&lt;/code&gt; → &lt;code&gt;0&lt;/code&gt; relative. Stored as absolute &lt;code&gt;0&lt;/code&gt;. We're back to checking &lt;code&gt;"foobar"&lt;/code&gt; again. &lt;code&gt;false&lt;/code&gt;. Advance → &lt;strong&gt;idx = 3&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And we're looping: 3 → 7 → 3 → 7, forever. The function never returns. The exact trigger is &lt;em&gt;"the first occurrence of the object is embedded in a longer word, and a later occurrence is standalone"&lt;/em&gt; — an input that's not exotic at all once real query strings are involved.&lt;/p&gt;

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

&lt;p&gt;The fix is to keep the two notions of position separate: a &lt;code&gt;base&lt;/code&gt; that tracks where the current search slice begins in absolute terms, and a &lt;code&gt;rel&lt;/code&gt; that's the relative result from &lt;code&gt;strings.Index&lt;/code&gt;. The absolute position is just &lt;code&gt;base + rel&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;objectIsQueried&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// strings.Index returns a relative offset within (*query)[base:], so we&lt;/span&gt;
        &lt;span class="c"&gt;// must add base to get the absolute position before passing it to&lt;/span&gt;
        &lt;span class="c"&gt;// isWholeWord (which operates on the original string).&lt;/span&gt;
        &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;abs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isWholeWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&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="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;abs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&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 changed, five removed. The slice-based search is still efficient (slicing a string in Go shares the underlying storage — no copy), but now every index handed to &lt;code&gt;isWholeWord&lt;/code&gt; is a true absolute position, and &lt;code&gt;base&lt;/code&gt; only ever moves forward. No more oscillation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pinning it with a regression test
&lt;/h2&gt;

&lt;p&gt;A fix you can't prove is a fix you'll lose. The change that actually matters long-term is the regression test, which encodes the exact input that used to hang so nobody can quietly reintroduce the bug later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Key regression case: first occurrence embedded, second standalone.&lt;/span&gt;
&lt;span class="c"&gt;// The original code looped forever on this input.&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"foobar foo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"foo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;alongside the ordinary cases (&lt;code&gt;"foo"&lt;/code&gt; in &lt;code&gt;"select foo from bar"&lt;/code&gt; → true, &lt;code&gt;"foobar"&lt;/code&gt; → false, punctuation boundaries like &lt;code&gt;"(foo)"&lt;/code&gt; → true, empty query → false), plus a separate table-driven test for &lt;code&gt;isWholeWord&lt;/code&gt; itself. The point isn't coverage for its own sake — it's that the one input that broke the function is now a named, permanent test case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When you slice before searching, every offset is relative.&lt;/strong&gt; &lt;code&gt;strings.Index(s[base:], x)&lt;/code&gt; does not return a position in &lt;code&gt;s&lt;/code&gt;. This is the kind of mistake that's invisible on a quick read and obvious the moment you trace a concrete input — which is the whole argument for tracing concrete inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hang is a bug, even without a crash.&lt;/strong&gt; No panic fired here. The only symptom would be a wedged goroutine. Code that loops on an external &lt;code&gt;Index&lt;/code&gt; result should always have a strictly-monotonic advance you can point to — here, proving &lt;code&gt;base&lt;/code&gt; always increases is the proof the loop terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading unfamiliar code is a skill worth practicing in public.&lt;/strong&gt; I found this by reading, not by hitting it in production. Open source is one of the few places you can do that on a real, used codebase and have your fix reviewed by people who maintain it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is up as a proposed change against KubeStellar — issue &lt;a href="https://github.com/kubestellar/kubestellar/issues/3848" rel="noopener noreferrer"&gt;#3848&lt;/a&gt; for the report, PR &lt;a href="https://github.com/kubestellar/kubestellar/pull/3849" rel="noopener noreferrer"&gt;#3849&lt;/a&gt; for the fix and tests. If you spend time in Go codebases, the relative-vs-absolute slice trap is worth keeping in your peripheral vision; it hides well.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Vignan Nallani. Found a bug like this, or want to talk through how you'd have traced it differently? I'm always up for it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>kubernetes</category>
      <category>opensource</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
