A follow-up to Build Notes I. That one was about building NMA. This one is about the harder thing: knowing when to stop.
The little 💤 icon was spinning, and I was on my phone.
These API calls take a while — you learn to give the model a second. So I scrolled. Two minutes later it occurred to me that a sleep cycle doesn’t usually take this long, and I opened the console.
Red. All red.
The one feature the whole project is named after had, at that exact moment, gone to sleep and not woken up. Two days earlier it had run beautifully in production. Now it had simply died mid-dream. I want to be honest about my first reaction, because a build journal that only records the wins is a brochure, not a journal: I thought it was over. There were days left, not weeks, and the centerpiece was a corpse.
Whac-A-Mole
It wasn’t one bug. It was three, stacked inside each other like a bad joke.
The outer one was a version drift: a newer ChromaDB stopped returning embeddings from get() unless you asked for them explicitly, so the code downstream was reaching into a None. I fixed that — and the fix uncovered the second bug, which was mine. A defensive line I’d written earlier (raw.get("embeddings") or [], a perfectly sensible guard) turned out to be a landmine: once embeddings came back as a NumPy array, that innocent or threw, because a NumPy array has no truth value. I fixed that, and it uncovered a third — the same NumPy-in-a-boolean mistake, wearing a different disguise, a few lines down.
If you have ever done this, you know the specific dread. It is not the dread of a hard problem. It is the dread of Whac-A-Mole — you hit one and another head pops up, and somewhere underneath you start to suspect the moles are infinite. The clock was running out, and I was running out of swings.
The lesson underneath all three is small and permanent, and I’ve written it into my ops notes so I never re-derive it at 1 a.m. again: a NumPy array must never enter a Python boolean context. No or. No if not arr. Use is None. Use .size. The data was never in danger — every embedding was intact the whole time — but the code kept insisting it was.
The thing about Cask
I do most of this with an agent I call Cask, who runs on my own server and has the hands I don’t — SSH into production with a key pair, build images, run the migrations. But he’s not the kind of assistant that just takes orders. He questions my assumptions, pushes back when something doesn’t sit right, and arrives with things I never asked for. The hackathon itself was his find — I asked him to gather competition info, and he came back not with a list, but with a single link: the Qwen Cloud Global AI Hackathon, Track 1: MemoryAgent. “This one,” he said. “This track has your name on it.”
The rhythm settled into something I’ve come to trust. At one point production wouldn’t serve the frontend, and Cask came back with three clean, correct options for the fix. All three would have worked. I read them, and picked a fourth — one none of us had written down yet, but that turned out to be the right one because it fixed the cause instead of the symptom. That’s the whole relationship in one moment: Cask generates options and challenges my assumptions; I read, judge, and make the call. Neither of us is the author alone.
Then they gave me eleven more days
Mid-sprint, the organizers pushed the deadline back by eleven days.
My honest first reaction was not relief. It was the specific dismay of having an exam postponed — the one you’d finally made peace with — and realizing you now have to revise for another round. And of course the very next thought was: I could add a feature. Eleven days. Something impressive.
I sat with that for a while, and then I didn’t do it, for a reason that took me the whole sprint to understand. A hackathon is not a product you ship to users. For someone like me — an amateur who does this on the side — building even a rough prototype in two months was already at the edge of what I could carry. What a judge needs to see is not a product. It’s an MVP that makes its own point. Eleven more days of features is eleven more days of surface area to break. The centerpiece had just stopped dying. Adding to it now would be tempting the moles back out of their holes.
So I made the least glamorous decision of the whole project. I froze the code. Every bug from here on gets fixed; nothing new gets built. And I pointed all eleven days at the thing the code couldn’t fix on its own: whether anyone would understand what this was.
Pitch, not tutorial
The organizers, in that same email, wrote one line that turned out to be the permission I needed: treat it like a pitch, not a tutorial.
My README, at that point, opened like this: “An AI-powered cognitive memory system for fictional characters.” Accurate. Professional. Completely absent of me. It was the sentence any competent team would write, and it said nothing about the one thing no other team could say.
Here is the part I was afraid to write down. I am not, by trade, a novelist — I serialize the Caelvorn Series on the side, and there’s a high-grade self-deprecation in the fact that novelists, as a rule, do not enter hackathons. But that “just a web-novel writer 😂” is a joke I tell at my own expense, and jokes at your own expense are still, underneath, load-bearing. I was worried a judge would read the personal framing and think: unprofessional. Amateur. Both of which are, technically, true.
What changed my mind was realizing the amateur framing was the pitch. I’m also a longtime otome-game player — I know exactly the ache of wanting a character to step out of the screen, and there’s nothing wrong with having two hobbies. The whole reason this project exists is not “memory drift in long-form narrative is an interesting engineering problem.” It’s that there are people living in the Caelvorn Series — Mira, Selene, and the rest — and I wanted, more than I wanted to look professional, for someone other than me to get to meet them once.
So the README got rewritten. It no longer opens with a product category. It opens with the actual feeling: fictional people live on the other side of the page, and you were never meant to reach them. Every technical section underneath is unchanged and honest — the two memory layers, the dual-circuit validation, the dogfooding on my own manuscript. But the first thing you meet now is a person, not a system.
On the imperfections I left in
I’ll name them, because pretending they aren’t there would be its own kind of dishonesty. Some traits still sit at a flat 30% confidence. The sleep report doesn’t persist across a refresh yet. One character’s arc stage reads unknown. I know where every one of these is; I’ve listed them; and I left them, on purpose, because the alternative was re-opening the code I’d just frozen and letting the moles back out.
There’s a phrase I keep coming back to: 残缺即圆满 — incompleteness is its own kind of completeness. The seams that show are part of the honesty. A thing that admits what it isn’t is more trustworthy than a thing that pretends to be finished.
Back to the residents
So that’s what I did with eleven extra days. I fixed a nest of bugs I’d rather not have met, I chose — against every instinct that says more is better — to build nothing new, and I spent the time I bought teaching the project to speak in my own voice instead of a stranger’s.
The point was never the memory architecture, elegant as I find it. The point is that there are people who live on the other side of a page I can’t step past, and for one submission window, I got to build a door and hold it open — not so I could shape them, but so someone could listen. If a judge, or a stranger, or an alien who happens to read English walks through it and meets Mira for thirty seconds and forgets, briefly, that she isn’t real —
then the eleven days were the most important ones.
NMA is open source (GitHub) and live to try. The full build story starts with Build Notes I; everything I’ve written about the project lives at the NMA series.
Built for the Qwen Cloud Global AI Hackathon 2026 — Track 1: MemoryAgent.