Curry Nomad started as an attempt to simplify Nora, a hackathon assistant I'd built around calendars, email and Google Workspace. I gave the new version a fictional Sri Lankan spice business and tried to make the responsibilities easier to see.
Then it grew. The repository is still called curry-nomad, but the app lives in apps/nora, and it has a supervisor, subagents, OAuth and memory again. Keeping the demo useful kept giving me reasons to add things. Going back through it was a chance to check whether the boundaries I'd planned had survived.
Different jobs need different amounts of freedom
An analytics question can be open-ended. The model may need to inspect tables, choose a query and correct it. An order reservation is much less flexible: stock either exists or it doesn't. I didn't want those two jobs handled by the same kind of loop.
The analytics agent gets schema-discovery and read-only SQL tools. An expected query error comes back as a SqlError it can try to correct. Other failures are allowed to surface instead of being treated as another opportunity to improvise.
Marketing follows a more predictable workflow: gather product facts, propose concepts, choose one, write copy and generate visuals. A person reviews the choices along the way. I can parallelise independent ideas without letting the whole process wander.
Orders, inventory and routing live in ordinary services. The agent calls them, but the services decide whether an operation is valid.
I described the stock check incorrectly
In an earlier write-up, I described a conditional update that would reserve stock only when enough was available. When I read the implementation again, it was doing a separate read, check and write:
stock = conn.execute("SELECT on_hand, reserved FROM stock_levels WHERE product_id = ?", (pid,)).fetchone()available = (stock["on_hand"] - stock["reserved"]) if stock else 0if qty > available:log.info("ops.oversell_rejected", product_id=pid, requested=qty, available=available)raise ...# ... then, separately:conn.execute("UPDATE stock_levels SET reserved = ? WHERE product_id = ?", (stock["reserved"] + qty, pid))
That code depends on the transaction around it:
conn.execute("BEGIN IMMEDIATE")
SQLite's BEGIN IMMEDIATE serialises writers. A second reservation can't read and update alongside the first; it has to wait or fail to acquire the write lock. That is what protects the stock check in this implementation.
Moving the same read-and-write code to Postgres without equivalent locking would need another review. Two transactions could read the same stock value and then overwrite each other's reservation. I'd prefer a conditional update or an explicit locking strategy, backed by a concurrent test. My original explanation had described the implementation I expected to find instead of the one I'd written.
The identity check found a bigger gap
I'd already written that the prototype needed more work on authentication and tenancy. The audit made that much more specific. In the configuration I reviewed, authentication was set to noop, so requests shared the anonymous identity. There was a token-verification handler, but it wasn't enabled.
There was no organization or tenant model. The memory store used shared namespace roots, and a Context.user_id field wasn't used to separate data. I'd also described the memory in terms that suggested a library which wasn't actually installed.
For a seeded demo, those findings explain its limits. For a shared application, they are work that needs doing before people bring their own data. A confirmation dialog doesn't solve this: the service still has to know who is confirming and what they're allowed to access.
Some questions remained open after the audit, including whether one namespace mismatch stopped the seeded brand memory being read on the platform path. I left those as unverified instead of turning a source-level suspicion into a claimed runtime failure.
What I'd carry into the next version
The split between analytics, marketing workflows and business services is still useful to me. It makes the model's role easier to explain and test. But I concentrated on that split before giving enough attention to the identity of the person using it.
The next version needs both. It should know which operation the agent may request and which caller is allowed to request it. Rereading the code against my own write-up exposed that gap much more clearly than another polished demo would have.