<?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: Nidhi</title>
    <description>The latest articles on DEV Community by Nidhi (@hereforlolz).</description>
    <link>https://dev.to/hereforlolz</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%2F3306519%2Fc0fd969b-4a6d-4e32-8687-134e64e4be96.png</url>
      <title>DEV Community: Nidhi</title>
      <link>https://dev.to/hereforlolz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hereforlolz"/>
    <language>en</language>
    <item>
      <title>What I didn't understand about MCP until I tried to use it</title>
      <dc:creator>Nidhi</dc:creator>
      <pubDate>Thu, 25 Jun 2026 13:33:26 +0000</pubDate>
      <link>https://dev.to/hereforlolz/what-i-actually-didnt-understand-about-mcp-until-i-tried-to-use-it-1ff9</link>
      <guid>https://dev.to/hereforlolz/what-i-actually-didnt-understand-about-mcp-until-i-tried-to-use-it-1ff9</guid>
      <description>&lt;p&gt;I'd read about Model Context Protocol the way most people probably have. A few blog posts, a diagram with boxes and arrows, a vague sense that it's "how you give an LLM access to external tools." I felt like I got it.&lt;/p&gt;

&lt;p&gt;Then I tried to wire a Notion connection into a Slack bot and realized I didn't understand it at all. Not the concept, the concept's fine. What I didn't understand was what it actually feels like to use one, versus read about one. This is that gap closing, badly at first, while building TeamTrail for the Slack Agent Builder Challenge.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I thought MCP was
&lt;/h3&gt;

&lt;p&gt;My mental model going in was something like: MCP server equals an API your LLM can call directly, kind of like function calling but standardized. Hand Groq a server URL, register the tools, done.&lt;/p&gt;

&lt;p&gt;Wrong. MCP doesn't talk to your LLM. It talks to your code. You write an MCP client, your client calls the MCP server, the server gives back results, and you decide what to do with those results, including whether they go into a prompt at all. The protocol has nothing to do with Groq, OpenAI, or Claude specifically. It's just a standard shape for your app to talk to an external service.&lt;/p&gt;

&lt;p&gt;Once that clicked, a bunch of confusing documentation stopped being confusing. Most guides blur "how MCP works" with "how a specific chat client uses MCP" like they're the same layer. They're not.&lt;/p&gt;

&lt;h3&gt;
  
  
  There are two kinds of MCP server and they assume different things about you
&lt;/h3&gt;

&lt;p&gt;I wanted Notion content in my briefings. Notion ships an official MCP server, so I figured this would be quick.&lt;/p&gt;

&lt;p&gt;Notion's MCP server comes in two forms though. The one most docs lead with, the hosted version where you click connect and do OAuth, assumes a human is sitting at a chat client clicking "Authorize" when the connection happens. My bot is a background process. There's no human there, no browser tab to redirect to. OAuth needs somebody to click something, and nobody's clicking anything on a server.&lt;/p&gt;

&lt;p&gt;I didn't know this distinction existed until I'd half set up the wrong one and hit a wall. The fix was simple once I knew to look for it, Notion also ships a local server you run yourself with a static integration token instead of an interactive login. But I want to flag this specifically because I think it's the most common trap for anyone using MCP from a backend service instead of a chat app: most MCP onboarding content is written for the chat-app case, and those assumptions don't automatically carry over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the server taught me MCP has its own auth layer, separate from the one you're thinking about
&lt;/h3&gt;

&lt;p&gt;Got the local server running. Tried to connect. Got back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Unauthorized: Missing bearer token"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first thought was that my Notion token was wrong. It wasn't. There are two different credentials doing two different jobs here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Notion integration token authenticates the server to Notion's API. This is the one I'd been thinking about.&lt;/li&gt;
&lt;li&gt;The MCP server also generates its own random bearer token to protect its own local HTTP endpoint. I hadn't considered this existed at all, because nothing in how I'd pictured "client talks to server" included "and the server also has its own front door lock."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I saw it framed that way it made sense. Of course a server listening on a port wants its own access control, separate from whatever it does with that access once granted. I just hadn't built that into my model yet. For a local, loopback-only server in a dev sandbox, disabling that extra lock is the right move since nothing outside the machine can reach it anyway, but I had to understand why the lock existed before I trusted myself to turn it off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calling a tool and getting back something I didn't expect
&lt;/h3&gt;

&lt;p&gt;Finally connected. Called the search tool. Got a result. Tried to read it as text I could drop into a prompt.&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%2Fqpho5g8l3z1llunxkhzk.gif" 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%2Fqpho5g8l3z1llunxkhzk.gif" alt="Right token" width="245" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It wasn't text. It was a JSON string, Notion's actual API response, serialized, sitting inside the MCP response's text field. I'd assumed the name "model context protocol" meant the content would arrive pre-formatted for an LLM to read directly. It doesn't. MCP standardizes the call itself, tool names, arguments, a content array, but what's inside that content is entirely up to the server. In Notion's case that's its raw API output, completely unprocessed.&lt;/p&gt;

&lt;p&gt;I also assumed search would return something I could summarize. It returned page titles and URLs. Nothing else. To get actual content I needed a second, separate tool call: fetch the page by ID, as markdown. Search finds things. It doesn't read them for you.&lt;/p&gt;

&lt;p&gt;None of this is bad design on Notion's part looking back. It's just not what I'd pictured, and the only way I found out was writing a tiny standalone script that connected, listed the available tools, and printed whatever came back, before writing a single line of code that assumed anything about the shape of the response.&lt;/p&gt;

&lt;p&gt;That habit, verify the real shape before writing logic against an assumed one, is the actual lesson here, more than any Notion-specific detail. I understand MCP now the way I understand most protocols I've actually had to build against: less a clean abstraction, more a real thing built by real people with specific decisions I have to go look at directly instead of guessing about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where that leaves me
&lt;/h3&gt;

&lt;p&gt;TeamTrail grounds its onboarding briefings in both Slack, via Real-Time Search, and Notion, via that local MCP connection, with citations back to both. But the thing I actually want someone to take from this isn't the feature. "I read about MCP" and "I've used MCP" are two different states, and the gap between them was made of small, specific things I only found by trying and being wrong first.&lt;/p&gt;

&lt;p&gt;So if you're about to wire up your first MCP server, don't start with the integration. Start with a script that does nothing but connect, list the available tools, call one, and print exactly what comes back. Read that output before you write a single line that assumes anything about its shape. It won't look like you think it will, and finding that out on purpose costs you ten minutes instead of an afternoon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest addendum, in hindsight.
&lt;/h3&gt;

&lt;p&gt;All three detours had the same root cause. Same fix too. I knew it before I started.&lt;br&gt;
"Connect, list tools, call one, print the raw output." Not just advice for whoever's reading this. It's the move I make on every system I touch, write code for it or not. Don't trust the mental model. Go look at the real thing.&lt;br&gt;
That's the TPM instinct showing up here more than the engineering one. I didn't need to write an MCP server from scratch to get this working. I needed to understand the protocol well enough to know where my assumptions were probably wrong, then go check them one at a time. Auth assumptions, response shape assumptions, all of it. The fix was never a deep coding skill. It was the same pattern-spotting I'd apply to any system I'm trying to make sense of fast.&lt;br&gt;
I had the real server sitting right there the whole time. Querying it directly would've cost ten minutes instead of time spent debugging code written against a guess I never checked.&lt;/p&gt;

&lt;p&gt;Code's on GitHub: github.com/Hereforlolz/slack-ai-playground&lt;br&gt;
Demo: &lt;iframe src="https://player.vimeo.com/video/1204668619" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Built for the Slack Agent Builder Challenge 2026.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>slack</category>
      <category>notion</category>
      <category>agents</category>
    </item>
    <item>
      <title>Dead Code Finder: GitLab Orbit-based static analysis that turned out to be harder than expected</title>
      <dc:creator>Nidhi</dc:creator>
      <pubDate>Mon, 22 Jun 2026 21:18:39 +0000</pubDate>
      <link>https://dev.to/hereforlolz/dead-code-finder-gitlab-orbit-based-static-analysis-that-turned-out-to-be-harder-than-expected-4jgk</link>
      <guid>https://dev.to/hereforlolz/dead-code-finder-gitlab-orbit-based-static-analysis-that-turned-out-to-be-harder-than-expected-4jgk</guid>
      <description>&lt;p&gt;I built a GitLab Duo Agent Platform flow for a hackathon. The goal was simple: find code that nothing actually calls.&lt;/p&gt;

&lt;p&gt;Not "what breaks if I delete this." That question already has a dozen tools. I wanted the narrower one: does anything call this at all?&lt;/p&gt;




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

&lt;p&gt;A flow called Dead Code Finder. It queries GitLab Orbit's knowledge graph for CALLS and IMPORTS edges on every Definition node in the project, then sorts findings into three buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confident&lt;/strong&gt;: zero incoming edges, not behind a decorator, not an external entry point&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncertain&lt;/strong&gt;: ambiguous cases I can't fully resolve statically (inheritance, MRO dispatch)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipped&lt;/strong&gt;: decorator-based dispatch, test framework reflection, hardware entry points. Explicitly flagged, not silently dropped.&lt;/li&gt;
&lt;/ul&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%2Fh7b6dkk3mkbwvh9eyr72.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%2Fh7b6dkk3mkbwvh9eyr72.png" alt=" " width="800" height="1037"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One hard rule: never say "safe to delete." The report only says "no reference found in the static call graph, here is exactly what was checked."&lt;/p&gt;

&lt;p&gt;It posts one report and stops. Never modifies files, never opens an MR.&lt;/p&gt;

&lt;p&gt;The core logic lives in a SKILL.md that defines the Orbit traversal procedure step by step. The agent flow reads it, runs it, writes the report.&lt;/p&gt;




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

&lt;p&gt;Query the graph, check edge counts, flag anything with zero incoming CALLS or IMPORTS. Pretty mechanical.&lt;/p&gt;

&lt;p&gt;I expected the hard part to be writing clean classification logic. It wasn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The platform didn't cooperate — until it did
&lt;/h3&gt;

&lt;p&gt;Two things broke at runtime.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;query_graph&lt;/code&gt; and &lt;code&gt;get_graph_schema&lt;/code&gt; were declared in the flow's YAML config but weren't in the actual toolset when the flow ran. Separately, skill injection was unreliable — sometimes the flow received only the manifest entry for SKILL.md (name and description) instead of the actual procedure body.&lt;/p&gt;

&lt;p&gt;Both turned out to have explanations. The skill injection issue I handled by inlining the full procedure directly in the system prompt as a guaranteed fallback — the injected skill is preferred when present, but the inline copy is always there. The missing graph tools turned out to be a per-user account setting: GitLab ships an "Orbit in GitLab Duo" preference (User Settings → Preferences) with separate opt-in toggles, and the "Custom Agents" toggle was off by default. Enabling it gave the flow &lt;code&gt;query_graph&lt;/code&gt;/&lt;code&gt;get_graph_schema&lt;/code&gt; access immediately.&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%2Fdaepg5jset8mww3bxcjn.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%2Fdaepg5jset8mww3bxcjn.png" alt="Enabling toggle" width="784" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I only found the toggle after building a file-based fallback mode. I kept both. The toggle can be off, skill injection can still fail independently, and a flow that degrades loudly is more useful than one that degrades silently.&lt;/p&gt;

&lt;p&gt;In fallback mode, the flow reads actual repo files using &lt;code&gt;list_repository_tree&lt;/code&gt;, &lt;code&gt;get_repository_file&lt;/code&gt;, &lt;code&gt;find_files&lt;/code&gt;, and &lt;code&gt;blob_search&lt;/code&gt;, applies the same heuristics, labels every finding &lt;code&gt;[INFERRED]&lt;/code&gt;, and opens the report with an explicit banner naming which tools were missing.&lt;/p&gt;

&lt;p&gt;No pretending to have graph evidence when I didn't.&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%2Fx0n250ercrgulvwossu4.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%2Fx0n250ercrgulvwossu4.png" alt="Agent session log showing fallback mode banner" width="800" height="679"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Static analysis is messier than it looks
&lt;/h3&gt;

&lt;p&gt;A few cases a naive edge-count check gets wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;__init__&lt;/code&gt;, &lt;code&gt;__enter__&lt;/code&gt;, &lt;code&gt;__exit__&lt;/code&gt;, &lt;code&gt;__del__&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These almost always look dead in a naive check. When you instantiate a class, Orbit registers a CALLS edge to the class, not to &lt;code&gt;__init__&lt;/code&gt;. So &lt;code&gt;__init__&lt;/code&gt; shows zero incoming edges even when the class is instantiated dozens of times. I confirmed this against a ~500-line SDK class (&lt;code&gt;EphemeralAgentExecutor&lt;/code&gt;) with a full unittest suite. Correcting the check to look at the enclosing class's incoming edges instead fixed the false positives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decorator-based dispatch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A function registered into a dict via a decorator and called later by string-key lookup is structurally indistinguishable from dead code in a static call graph. There is no literal call in source. These go in Skipped with the actual reason stated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test framework reflection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unittest.TestCase&lt;/code&gt; methods are discovered through reflection over class attributes, not through any literal call anywhere. The test runner finds them at runtime in a way the static graph can't see. Same bucket, same reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inheritance and MRO&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A method only reachable through a subclass that doesn't override it might show no direct incoming edges on the base class method itself. These go in Uncertain rather than flagged dead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validation
&lt;/h2&gt;

&lt;p&gt;I built test fixtures specifically to hit the tricky cases: plain unused functions, cross-file import resolution, inheritance/MRO, decorator dispatch, constructor/dunder handling, and unittest reflection.&lt;/p&gt;

&lt;p&gt;Two live runs in fallback mode correctly identified every planted dead-code fixture with cited file and line evidence, correctly excluded the decorator and test-discovery cases with reasoning for each, and correctly downgraded the ambiguous inheritance and script entry point cases to Uncertain.&lt;/p&gt;

&lt;p&gt;Then I ran the Orbit traversal procedure manually against the live graph via CLI to check whether fallback mode's file-based guesses matched actual graph data.&lt;/p&gt;

&lt;p&gt;Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;totally_unused_helper&lt;/code&gt; — zero incoming CALLS/IMPORTS edges. Actually dead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cross_file_dead_helper&lt;/code&gt; — zero incoming edges. Actually dead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;undecorated_dead_function&lt;/code&gt; — zero incoming edges. Actually dead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Base.greet&lt;/code&gt; — incoming edge from &lt;code&gt;Child.run&lt;/code&gt;. Reachable via inheritance. Fallback mode correctly put this in Uncertain rather than flagging it dead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;summarize_text&lt;/code&gt; — alive via script entry point. Correctly Uncertain.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__init__&lt;/code&gt;, &lt;code&gt;__exit__&lt;/code&gt;, &lt;code&gt;__del__&lt;/code&gt; — zero direct incoming edges, with usage only on the enclosing class. Constructor correction validated.&lt;/li&gt;
&lt;/ul&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%2F4zna90u9idh8jwb80qnf.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%2F4zna90u9idh8jwb80qnf.png" alt=" " width="800" height="1219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph also surfaced something fallback mode couldn't confirm: &lt;code&gt;add_temp_file&lt;/code&gt; and &lt;code&gt;issue_credential&lt;/code&gt; show zero incoming CALLS edges. Potential genuine dead code that file-reading alone couldn't settle.&lt;/p&gt;

&lt;p&gt;Every fallback finding held up against the graph.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the first Orbit run found
&lt;/h3&gt;

&lt;p&gt;Once the Custom Agents toggle was on, I re-triggered the flow live and ran it against the graph for the first time from inside the flow itself. It worked — and immediately turned up two bugs in the procedure logic.&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%2Fk1gy0d9hs3sq43z9ncfq.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%2Fk1gy0d9hs3sq43z9ncfq.png" alt="First Orbit run. use_it is row 5 in Bucket A — flagged as confidently dead. It isn't." width="800" height="1370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: standalone driver functions misclassified as Bucket A.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;use_it()&lt;/code&gt; is a top-level function that instantiates &lt;code&gt;Child&lt;/code&gt; and calls &lt;code&gt;.run()&lt;/code&gt; on it to exercise the MRO chain. It has zero incoming CALLS edges, because nothing calls it — it's meant to be run directly. The flow flagged it as confidently dead. It isn't. It's the same shape as a single-definition &lt;code&gt;main&lt;/code&gt; file: an entry point that external tooling invokes, just not literally named &lt;code&gt;main&lt;/code&gt;. The procedure now explicitly checks whether a zero-edge function's body instantiates a class and chains method calls — if so, it goes to Bucket B (Uncertain), not Bucket A.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: a genuine call edge came back as zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cross_file_used_helper&lt;/code&gt; is directly called by &lt;code&gt;cross_file_test_caller.run()&lt;/code&gt; — a simple, one-hop call. The batched query returned zero edges for it anyway. A separately observed issue in this environment: 19-digit Definition IDs sometimes come back truncated by one digit in query responses, which produces a false "no edges" result for nodes that are genuinely called. The fix: when a simple, expected edge comes back as zero, re-run that single node ID on its own before trusting the result. The solo re-check resolved correctly — &lt;code&gt;cross_file_used_helper&lt;/code&gt; is alive and doesn't appear in any bucket.&lt;/p&gt;

&lt;p&gt;A second run after both fixes corrected both cases, and independently re-confirmed &lt;code&gt;add_temp_file&lt;/code&gt; and &lt;code&gt;issue_credential&lt;/code&gt; as Bucket A, matching the CLI finding from before.&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%2F9q546ch3lgx5tjwspma7.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%2F9q546ch3lgx5tjwspma7.png" alt="call-graph analysis" width="800" height="545"&gt;&lt;/a&gt;&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%2Fgo0ujf6q99siyf0ozhcz.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%2Fgo0ujf6q99siyf0ozhcz.png" alt="The full report from that run. use_it is in Bucket B — but it shouldn't have been there at all in the first run. More on that below." width="800" height="1391"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Confirm which tools are actually available in the runtime before writing any logic that depends on them. Building the fallback mode wasn't hard, but designing for both paths from the start would have been cleaner than retrofitting it.&lt;/p&gt;

&lt;p&gt;Also: run the flow against the graph earlier in development. The two procedure bugs above only showed up once &lt;code&gt;query_graph&lt;/code&gt; calls were running — fallback mode's file-reading was too weak to surface them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's still open
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Extend validation past Python&lt;/li&gt;
&lt;li&gt;Transitive dead code detection (code only referenced by other dead code)&lt;/li&gt;
&lt;li&gt;References from non-code files: YAML, CI templates, cron jobs&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The thing I actually learned
&lt;/h2&gt;

&lt;p&gt;A report that says "I don't know" in the cases where it genuinely doesn't know is more useful than one that sounds confident everywhere and is occasionally wrong.&lt;/p&gt;

&lt;p&gt;That ended up applying to the platform too. Once I confirmed the Orbit tools weren't available, the right call was to say so loudly in the report, not mask it with a fallback that looked like the real thing. And when the tools became available and the first run found bugs, the right call was to fix the procedure and run again rather than rationalize the bad output.&lt;/p&gt;

&lt;p&gt;The three-bucket structure came from the same instinct. Most dead-code tools give you a flat list. The flat list trains you to ignore it once it's wrong a few times. Labeling the uncertainty explicitly is what makes the confident findings actually worth acting on.&lt;/p&gt;

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

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1203694432" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gitlab</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Memory Agent That Actually Forgets (And the Three Bugs That Taught Me Why That's Hard)</title>
      <dc:creator>Nidhi</dc:creator>
      <pubDate>Sun, 21 Jun 2026 14:38:50 +0000</pubDate>
      <link>https://dev.to/hereforlolz/building-a-memory-agent-that-actually-forgets-and-the-three-bugs-that-taught-me-why-thats-hard-526</link>
      <guid>https://dev.to/hereforlolz/building-a-memory-agent-that-actually-forgets-and-the-three-bugs-that-taught-me-why-thats-hard-526</guid>
      <description>&lt;p&gt;&lt;em&gt;By Nidhi: Built for the Global AI Hackathon Series with Qwen Cloud, Track 1: MemoryAgent&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I signed up for Track 1, I figured memory was basically a vector database problem. Store embeddings, retrieve similar messages, done. The first prototype worked fine. Then I started actually trying to use it, and that's when things got interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Most AI chat is stateless. Close the tab, and the next conversation starts from zero. That's a strange limitation for something we keep calling "intelligent": a person who knew you well wouldn't forget your name, your job, or what you told them you were working on, three days ago. So the plan was: build a memory layer underneath a chat agent. Every turn would recall relevant past context, respond with it in mind, and extract new facts worth remembering. Important things stick around. Trivial things expire.&lt;/p&gt;

&lt;p&gt;That part came together quickly. FastAPI for the backend, split into two services: a memory core handling storage and intelligence, and a thin chat layer on top calling into it. Qwen Cloud for both the chat model and the embeddings. Neon Postgres with pgvector for the actual vector storage, Upstash Redis for caching, all of it deployed on Alibaba Cloud ECS. Within a couple of days I had a working chat UI that stored memories, scored them for importance, and recalled them on the next message.&lt;/p&gt;

&lt;p&gt;It looked done. It was not done.&lt;/p&gt;

&lt;p&gt;Getting there wasn't clean, either. Two FastAPI services that needed to actually talk to each other, a Postgres instance on Neon that needed pgvector enabled before anything would even insert, a Redis cache that silently no-ops if it can't connect instead of telling you it failed, and then the actual deploy — SSH into an Alibaba Cloud ECS box, realize the venv isn't activated by forgetting which environment you are in, watch both services crash with &lt;code&gt;ModuleNotFoundError&lt;/code&gt;, activate it, restart, repeat. None of that shows up in an architecture diagram. All of it ate real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug one: the system was punishing specificity
&lt;/h2&gt;

&lt;p&gt;I started running real conversations through it, not test data, just talking to it the way a user would. At one point I told it my name. A day later, I asked what it remembered, and my name had a 24-hour expiration timer on it. Separately, I'd mentioned growing cherry tomatoes and basil, and that specific fact scored &lt;em&gt;lower&lt;/em&gt; than the vague, generic statement "I'm an indoor gardener" that I'd said in the same conversation.&lt;/p&gt;

&lt;p&gt;That's backwards in a way that matters. The entire value of a memory system is recalling specifics — "user grows cherry tomatoes" is useful; "user likes gardening" barely is. But the scoring prompt I'd written only gave Qwen three anchor points: trivial greetings at 0.0, vague preferences at 0.5, and core facts at 1.0. There was no guidance at all for "specific personal fact" or "person's own name," so it was filling in the gap with something closer to vibes than calibration.&lt;/p&gt;

&lt;p&gt;The fix was to stop assuming the model would infer the right priorities and instead write them down explicitly: a name should never score below 0.6. A specific instance of something should never score lower than the general category it belongs to. Once I added those as hard calibration rules rather than implicit expectations, both problems disappeared. The lesson that stuck with me: when you're using an LLM as a scoring function, it will dutifully fill in whatever gaps you leave in your rubric, in whatever direction is easiest- not necessarily the direction you'd want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug two: "save this" did nothing, and said it worked anyway
&lt;/h2&gt;

&lt;p&gt;This one was sneakier because it &lt;em&gt;looked&lt;/em&gt; like success. I'd ask the agent for something — a training plan, a morning routine — and then say "save this for me." It would reply with a perfectly confident confirmation: &lt;em&gt;"I've updated my memory with your specific training plan."&lt;/em&gt; And then I'd check the actual memory panel. Nothing relevant was there.&lt;/p&gt;

&lt;p&gt;The bug was structural. My extraction step — the part that pulls durable facts out of a conversation turn to store — only ever looked at the current turn: the user's message and the assistant's reply, nothing else. When the user's message was "save this," there was nothing in that extraction call's view of the world to actually save. The real content — the training plan — had been generated several messages earlier, in a different turn the extractor never saw. The chat reply that &lt;em&gt;generated the confirmation&lt;/em&gt; had full conversation history available to it, so it sounded confident and specific. The extraction call that was supposed to &lt;em&gt;act&lt;/em&gt; on that confirmation had none of that context. Two parts of the same pipeline were operating with completely different amounts of information, and nothing in the architecture made that mismatch visible until I went looking for it by hand.&lt;/p&gt;

&lt;p&gt;The fix was to feed recent conversation history into the extraction prompt too, with explicit instructions: if the user is pointing back at something said earlier rather than stating a new fact directly, resolve the reference using that history before deciding what to extract. After the fix, the same "save this" request correctly pulled out the actual content of whatever it was referring to.&lt;/p&gt;

&lt;p&gt;What stuck with me here wasn't the fix — it was how &lt;em&gt;convincing&lt;/em&gt; the failure was. A wrong answer that sounds tentative gets double-checked. A wrong answer delivered with total confidence usually doesn't. I only caught this because I happened to check the memory panel out of habit, not because anything in the system signaled a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug three: the feature that had never once worked
&lt;/h2&gt;

&lt;p&gt;Smart Forget was supposed to be one of the more interesting pieces.Instead of blindly deleting memories the moment a timer expired, it would gather anything past its expiration window and ask Qwen to make one more judgment call: does this still matter, even though its time technically ran out? I built it, tested it, and every single run reported back &lt;code&gt;reviewed: 0, deleted: 0&lt;/code&gt;. I assumed that meant nothing had expired yet in my short test windows, which was plausible — TTLs were measured in days, and I'd only been testing for a few hours at a time.&lt;/p&gt;

&lt;p&gt;It wasn't that. It was a crash, quietly swallowed by my own error handling. The database returns timestamps as timezone-aware datetime objects. My code was comparing them against &lt;code&gt;datetime.utcnow()&lt;/code&gt;, which is timezone-&lt;em&gt;naive&lt;/em&gt;. Python refuses to subtract one from the other, throws a &lt;code&gt;TypeError&lt;/code&gt;, and my &lt;code&gt;try/except&lt;/code&gt; block caught that error and defaulted to "keep." The scary part: nothing crashed loudly enough to notice. My own error handling turned a real failure into something that looked exactly like cautious, conservative behavior — when really, the feature had never run a single actual comparison.&lt;/p&gt;

&lt;p&gt;I only found this because I built a real test suite, not unit tests with mocks, but a script that makes live HTTP calls against the actual running deployment and checks real outcomes. And even then, the test initially passed, because &lt;code&gt;0 deleted&lt;/code&gt; wasn't technically wrong; it just wasn't &lt;em&gt;meaningfully right&lt;/em&gt; either. The thing that actually caught it was going one level further: manually pushing a memory's expiration into the past directly in the database, triggering Smart Forget, and watching the server logs in real time. That's when the actual &lt;code&gt;TypeError&lt;/code&gt; showed up, instead of being hidden behind a graceful-looking fallback.&lt;/p&gt;

&lt;p&gt;The fix itself was small: switch to timezone-aware datetimes everywhere, fix the underlying schema too so the mismatch couldn't quietly recur. The bigger fix was procedural: a passing test and a correctly-behaving system are not the same thing, and the gap between them is exactly where things like this hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually taught me
&lt;/h2&gt;

&lt;p&gt;None of these three bugs threw an error message I noticed, or failed a test on the first pass. Two of them gave me a confident-sounding reply while doing nothing underneath. The third one looked cautious and careful while actually just being broken. Turns out a smooth reply, a 200 status code, and a "0 deleted" that I assumed meant "nothing needed deleting" are not the same thing as "this is actually working."&lt;/p&gt;

&lt;p&gt;What caught all three, every time: stop trusting the explanation and go check the data. Open the actual memory panel instead of reading the reply text. Tail the actual server logs instead of trusting the status code. Manufacture the edge case by hand instead of waiting for it to show up on its own. None of that is clever. It's just being willing to look one layer past whatever the system is telling you, instead of taking its word for it.&lt;/p&gt;

&lt;p&gt;The project that came out the other side does what the brief asked: it remembers what matters, lets go of what doesn't, and reconciles contradictions instead of accumulating them forever. But the part I'd actually want to talk about in an interview isn't the architecture diagram. It's the three different ways a system can look like it's working when it isn't, and what it took to notice each one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Qwen Cloud (&lt;code&gt;qwen-plus&lt;/code&gt; and &lt;code&gt;text-embedding-v3&lt;/code&gt;), FastAPI, Neon PostgreSQL with pgvector, Upstash Redis, and deployed on Alibaba Cloud ECS, for Track 1: MemoryAgent of the Global AI Hackathon Series with Qwen Cloud.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Demo video:&lt;iframe src="https://player.vimeo.com/video/1204609875" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hackathon</category>
      <category>webdev</category>
      <category>python</category>
    </item>
  </channel>
</rss>
