Inspiration

Freight dispatch runs on knowledge that never makes it into a system of record. Dock 4 at the Mississauga DC has a 13'6" canopy - no high-cubes. That ramp backs up Friday afternoons; cross later. This receiver won't take a drop trailer. It lives in the heads of the two people who have worked the desk longest, and it leaves when they do.

Meanwhile the TMS knows where the truck is and nothing about why last March's reroute went the way it did.

So we asked a narrower question than "can an agent remember?" — can an agent remember a place? Not "recall the last thing the user said," but: this shipment is 3 km from a closure on the 401, and there are two things the organization knows about this corridor and this receiving dock, and both are relevant because of where the truck is right now.

That question has a specific answer in CockroachDB, and it is the whole project.

What it does

Laine is a freight-dispatch copilot for the Greater Toronto Area whose entire memory layer is CockroachDB. A dispatcher works in a chat panel next to a map that the agent paints as it reasons.

It recalls because of location. Lane and site knowledge is stored with a vector embedding and a GEOGRAPHY point in the same row. Recall runs vector similarity and then bounds it spatially, so asking about a closure surfaces what the organization knows about that corridor — not what is merely semantically similar.

It scores real exposure. Live Ontario 511 road events (each carrying its own fetch timestamp) are joined against in-transit shipments with ST_DWithin, and each corridor gets an exposure score that weighs cargo value against time-to-SLA:

$$E = \frac{V}{10{,}000}\left(1 + \frac{4}{\max(1,\,h)}\right)$$

where $V$ is total cargo value on the corridor in CAD and $h$ is the hours remaining on the tightest SLA. A quiet corridor scores near zero; a corridor carrying a load due in two hours dominates.

It proposes, a human approves, and the approval is one transaction. An approved reroute computes a truck-costed detour (Valhalla, 4-axle tractor-trailer, with the closure point excluded so the path is a genuine detour and not the same road with a longer ETA), updates the shipment, rebooks the dock, seals the decision with the IDs of every memory and road event it rested on, promotes the memories a human just validated, and composes a dispatch notice with an RFC 5545 calendar invite for the dock window. All of it commits together or none of it does.

It refuses. Two carrier agents race for the same dock window, connected as two separate SQL identities created through the ccloud CLI. Both read the same index span, both find it free, both try to write. Under SERIALIZABLE, CockroachDB raises a real 40001 and exactly one booking survives. The loser's refusal names the carrier that won. There is no SELECT FOR UPDATE anywhere in that path - the whole point is that the refusal is real and observable, not a lock that quietly serializes the two agents into a story with no conflict in it.

Personal memory becomes institutional memory only when a human agrees. Anything the dispatcher teaches the agent is written to their own session. When a human approves a decision that rested on one of those memories, the same transaction copies it into the organization's session with provenance: who observed it, when, which memory it came from, and which decision promoted it. A unique partial index makes promotion idempotent — a memory can be promoted exactly once, enforced by the database rather than by application code. From then on, every dispatcher recalls it, and the agent says who taught it.

Inbound text is screened before it reaches the model. Carrier email is run through Amazon Bedrock Guardrails, and a blocked injection attempt is itself written as a memory — the security posture feeds the memory layer.

How we built it

One database, no side cache. A single CockroachDB row carries the text, its VECTOR(1024) Titan V2 embedding, a GEOGRAPHY point, an H3 cell, and a foreign key to the facility it describes. Transcripts are JSONB in the same database. Decisions, bookings, shipments, facilities, road events, and the agent registry are all relational tables beside it. There is no Pinecone, no Redis, no PostGIS sidecar. Every join the agent needs is a join, not a network hop — and consolidation is exactly what the rubric's first criterion asks about.

The vector index has a prefix column, and that is the multi-tenancy primitive.

VECTOR INDEX memories_vec (session_id, embedding vector_cosine_ops)

CockroachDB's C-SPANN index accepts leading prefix columns that must be constrained by equality or IN with constants. That single design choice is what lets one indexed scan serve WHERE session_id IN ($me, $org) — the dispatcher's personal memory and the whole organization's institutional memory, ranked together in one pass, with no union of two queries and no post-merge re-ranking.

It is also why memory promotion is an INSERT into the org session rather than a scope column on the row. A scope = 'org' predicate would have been a non-prefix filter, and the planner would have silently dropped the vector index for a full scan. The schema shape is dictated by the index shape.

Verification is a build step, not a vibe. scripts/verify-plans.mjs runs EXPLAIN against the live cluster and fails if the recall query stops using the vector index, so a future refactor that quietly degrades to a sequential scan breaks CI instead of the demo.

The interlock is proved by a test, not narrated. test-slot-race.mts runs both carriers against the real cluster and asserts: exactly one booking committed, at least one genuine 40001 was observed, zero orphaned decision rows, and the sequential re-run produces a clean refusal naming the winner. It exits non-zero on failure.

Stack. Next.js + the Vercel AI SDK for the tool loop (15 server-side tools), MapLibre for the map, Valhalla on OSM extracts for truck routing (a second Fargate service in the deploy), CockroachDB v26.2 (Docker locally, Cloud Basic for the judged cluster), and AWS for embeddings, guardrails, notification, and hosting.

Data provenance is stated, not implied. The facilities are 1,458 real logistics sites from Overture Maps. The road events are live Ontario 511. The shipments, trucks, docks, and carriers are a simulated exercise, and the UI says so on screen — once, plainly, where a judge will see it.

Challenges we ran into

CockroachDB has no spatial KNN. You cannot order by vector distance and filter by ST_DWithin and keep the index — the spatial predicate is not something the vector index can enforce. The shape that works is a hinted vector top-k with deliberate over-fetch inside a subquery and the spatial bound applied outside it. Getting it wrong costs you the index with no error message — and the multiplier is not a detail. We shipped k*4, which is correct for un-bounded recall because it discards nothing, and badly wrong under a spatial bound: the nearest memories are usually not the top vector matches, so the post-filter throws most candidates away. Measured on a live closure, four memories sat inside a 15 km bound and recall returned one. At k*16 the same query returns three, and the plan guard still confirms C-SPANN is load-bearing. The over-fetch has to scale with how selective the outer filter is.

schema_locked = true silently blocks ALTER TABLE. Adding the promotion columns failed until we inspected the table through the Cloud MCP server's get_table_schema and found the storage parameter. Every migration since brackets its DDL in an unlock/relock pair.

40001 and 40003 are not the same thing, and we had them wrong. The official CockroachDB agent skill designing-application-transactions corrected three real defects in this repo: retries with no backoff (we were hammering), 40003 ambiguous commits treated as retryable (which would double-insert a non-idempotent booking), and a connection pool with no lifetime or idle timeouts. Consuming that skill was worth more than reading the same material as prose, because it arrived as a checklist against our actual code.

Valhalla's exclude_locations can do nothing at all, and thirty metres was the difference. Our reroutes came back byte-identical to the direct route: same distance, same duration, same shape. Valhalla snaps an exclude point to the nearest graph edge, and a closure sitting beside the carriageway the route actually uses — an express lane next to a collector, the far side of a divided highway — snaps to an edge the route never touches. Measured against a live Pickering closure: 34.983 km / 24.0 min with the exclusion and without it. Snapping the avoid point onto the route's own returned geometry first moves it 30 metres and produces a real detour, 35.216 km / 25.0 min. Nothing on screen had been false — the sealed evidence reported detour_cost_minutes: 0 honestly the whole time — the reroute simply had no picture to show.

A memory pinned in the wrong place can never be recalled, and nothing tells you. Our seeded corridor knowledge was placed at the midpoint between two distribution centres regardless of what it described, so a note about the Windsor–Detroit tunnel sat in suburban Toronto, ~350 km from Windsor. Recall is bounded by ST_DWithin, which makes geography the index — and the failure is silent, because an un-recalled memory looks exactly like a memory that had nothing to say. We only caught it by filming: the agent sealed a reroute and reported evidenceMemories: 0, then explicitly declined to cite unrelated notes as justification rather than inventing one. Pinning each memory at the place it names took the nearest memory to a live closure from 326 km to 1.9 km. The Windsor and Kingston notes are still genuinely far away and correctly do not surface for a Toronto closure — that is the mechanism working, not a gap.

Accomplishments that we're proud of

The refusal is real. The easiest version of a "two agents race" demo uses a lock and narrates a conflict that never happened. Ours has no SELECT FOR UPDATE in the booking path on purpose, so the 40001 in the transcript is a genuine serialization failure that CockroachDB raised — and there is a test that fails if it stops happening.

All four CockroachDB tools are load-bearing, where two are required. Not four logos: a prefix vector index whose plan is guarded in CI; a ccloud bootstrap script that provisions the cluster, a service account, and the two carrier SQL identities the race authenticates as; a Managed MCP Server that did the schema inspection and EXPLAIN work during development; and the Agent Skills repo, which we consumed (three fixes above) and then wrote against — we authored a skill of our own in the same specification and ship it in this repo. (No upstream PR was filed; the skill is ours, in their format.)

Memory has a promotion path with a human in it. Most agent memory is written by the agent and read by the agent. Here, personal knowledge becomes organizational knowledge only through a human approving a decision that used it — and the database, not the code, guarantees it happens exactly once.

The agent refuses to overstate its own evidence, unprompted. Filming produced three moments we did not script: it sealed a decision and reported evidenceMemories: 0 rather than dressing up unrelated notes as justification; it declined to pin a memory to a facility name that does not exist in the network, flagging the naming question instead of inventing a location; and it stated plainly that a dispatch notice had been composed but not delivered because the notification provider was in mock mode. That last one is the provider-switch discipline surfacing itself to the user — the app will contradict a demo script that overclaims, which is the right way round.

What we learned

The index shape dictates the schema shape. We reached for a scope column the way anyone would, and it would have been a silent 10× regression rather than an error. Working backwards from what the C-SPANN prefix can constrain produced a better data model than working forwards from the domain.

A skill is a better delivery vehicle for database expertise than documentation. The transaction-design skill found three defects in code we had already reviewed, because it arrived as executable expertise pointed at our repository rather than as an article we would have nodded along to.

What's next for Laine

  • Row-level TTL as a forgetting curve. Lane knowledge decays at different rates — a construction closure is stale in weeks, a dock canopy height is true for years. TTL per memory kind, with confidence decaying toward the promotion threshold, is designed and not yet built.
  • Contradiction instead of accumulation. supersedes is in the schema; the agent does not yet reason about a new memory that contradicts an old one. That is the interesting half of institutional memory.
  • The carrier side of the race. Today the two racing identities are ours. The mechanism is a real multi-tenant booking protocol, and the natural next step is exposing it so a carrier's own agent connects as itself.

Built With

Share this project:

Updates

Submission history