ai newsSo., 18. Okt.
EilmeldungClaude Code 2.1.302 Flips Two claude -p Defaults: Permission Mode and Fork Subagents

I shut down the L train in a graph: max flow, min cut and AI agents on the New York subway

Picture 8 a.m. on a Wednesday in October. A signal fire in the Canarsie Tube stops the L between Bedford Av and 8 Av, in both directions, until further notice. That one sentence breaks the trips of 23,445 riders an hour . A transit control room now has minutes to answer three questions: who is stuc

dev.toSonntag 16:23 · 18.10.7 Min. Lesezeit
Bild: dev.to

Picture 8 a.m. on a Wednesday in October. A signal fire in the Canarsie Tube stops the L between Bedford Av and 8 Av, in both directions, until further notice.

That one sentence breaks the trips of 23,445 riders an hour. A transit control room now has minutes to answer three questions: who is stuck, where will they go, and can the rest of the network carry them?

A trip planner can't help much here. It finds one rider the fastest way from A to B. It can't tell you whether the network can absorb twenty-three thousand of them an hour, because that is a flow problem.

So for the WeMakeDevs × FalkorDB Graph Hacks hackathon I built Detour: agents on a FalkorDB graph of the real New York City subway that measure what the city can still carry, and test a fix before anyone is told.

Live demo: https://detour-orpin.vercel.app · Code: https://github.com/SaudSatopay/detour

A build script assigns every pair to its fastest route (in-vehicle time, half the headway as the wait, a transfer penalty), which gives every segment a baseline load. Only one segment comes out over capacity at baseline: the 5 from Grand Central to Union Square, at 101%. That was my sign the model was sane.

// scheduled segments (:Station ) - [:RUNS { route, capacity, load, minutes }] -> (:Station ) // complexes under 400 m apart (:Station ) - [:WALK { meters, minutes, capacity }] -> (:Station ) // 8 a.m. flows; via = the segment ids of the path (:Station ) - [:DEMAND { riders, via }] -> (:Station ) // a platform layer, so waits and transfers cost minutes (:Station ) - [:BOARD ] -> (:Platform ) - [:RIDE ] -> (:Platform ) (:Platform ) - [:ALIGHT ] -> (:Station ) Enter fullscreen mode Exit fullscreen mode And each job gets its own graph:

detour:template the network and seeded memory, never changed detour:sbx:<visitor> your own copy, made with GRAPH.COPY detour:fork:<...> a throwaway copy for each what-if detour:demand the 30,205 flows, shared and read-only Enter fullscreen mode Exit fullscreen mode Step 1: Who is hit? One MATCH Each DEMAND edge keeps the segment ids of its 8 a.m. path in a via property. That one property turns the first question into a single query:

MATCH ( o: Station ) - [ d: DEMAND ] -> ( x: Station ) WHERE any ( e IN d.via WHERE e IN $closed ) RETURN o.id AS o, x.id AS d, d.riders AS riders Enter fullscreen mode Exit fullscreen mode For the L between Bedford Av and 8 Av, that is 23,445 riders an hour across 2,021 station pairs. Only 7,823 of them board at the closed stations; counting outward, 14,713 board within four stops of the closure. An alert sent only to the closed stations would miss two thirds of the people affected.

Riders don't spread out politely. They take their fastest open path. To make waits and transfers cost something, the graph has a platform layer: a Platform node per station and route, BOARD edges that cost a three-minute transfer penalty plus half the headway, RIDE edges along the scheduled segments, and free ALIGHT edges. Then every displaced pair is rerouted in one query:

UNWIND $pairs AS p MATCH ( a: Station { id: p.o }), ( b: Station { id: p.d }) CALL algo.SPpaths ({ sourceNode: a, targetNode: b, relTypes: [ 'BOARD', 'RIDE', 'ALIGHT', 'WALK' ], weightProp: 'minutes' }) YIELD path, pathWeight RETURN p.o AS o, p.d AS d, pathWeight AS minutes Enter fullscreen mode Exit fullscreen mode All 2,021 pairs reroute in under a second. Left to themselves, L riders pile onto the G: four G segments run at 190 to 244% of capacity, and 4,689 riders an hour have no open rail path at all.

Fastest paths show what riders will do. Max flow shows what the network could do if they spread out perfectly.

Each call forks the visitor's graph with GRAPH.COPY, deletes the closed segments, frees the capacity the displaced riders were using, and wires a super source and a super sink to every station by its balance of displaced riders, once per direction of travel. Then:

MATCH ( src: Hub { id: $src }), ( snk: Hub { id: $snk }) CALL algo.maxFlow ({ sourceNodes: [ src ], targetNodes: [ snk ], relationshipTypes: [ 'FLOW' ], capacityProperty: 'spare' }) YIELD maxFlow, edges, edgeFlows RETURN maxFlow, edges, edgeFlows Enter fullscreen mode Exit fullscreen mode algo.maxFlow takes a single relationship type and no parallel edges, and a subway corridor is full of parallel edges: the 4, 5 and 6 run between the same stations, plus walks. So the fork sums the spare capacity per station pair into one FLOW edge and keeps the ids of the segments behind it. That maps the flow and the cut back to real lines.

The answer for the L: 17,167 of the 18,432 riders an hour headed into Manhattan can still move. That's 93%. The min cut sits at 1 Av. Its only rail links are the closed L, so no spare capacity anywhere else can reach its riders. That is exactly where a shuttle bus should start.

GRAPH.COPY makes a what-if cheap. The controller looks for shuttle bridges within 2 km of the stations the network can't clear. Then it copies the closure fork once per plan, adds the bus legs and runs the same tools again:

One result surprised me. With the shuttle, fastest-path overflow rises, from 24,078 to 28,762, because 4,066 riders who had no way through can now travel. So the plan also needs the rider desk to steer people onto lines with room. And for the other two closures in the demo, the Lexington express and the Cranberry Street Tunnel, the open network absorbs everyone, so the controller proposes steering riders and no buses at all.

The model's job is to choose tools and explain the results, not to invent queries or numbers. The agents pick from a fixed catalogue of 13 typed tools, backed by 83 queries. Each tool returns one of four answer shapes (answered, from memory, not in the graph, unknown name), a one-line summary, the data, and the exact Cypher it ran with its timings. The control room shows all of it, step by step.

dispatcher | v controller agent --- 13 typed tools, 83 queries ---> FalkorDB | v a person approves | HANDED_OFF v rider desk: one agent per remembered rider Enter fullscreen mode Exit fullscreen mode Nothing reaches riders until a person approves a plan. Then the controller hands off to the rider desk, a HANDED_OFF edge records it, and one rider agent per remembered rider writes a new route, step-free where that rider needs it.

Detour runs on free model tiers, and Gemini's free tier allows only a few full runs a day. So I also wrote the controller's procedure as code. When the quota runs out, the same tools run in the same order on the visitor's graph, and the run is labeled scripted. When the database is unreachable, a run recorded on FalkorDB plays back, labeled as a recording. The same tools are also served over MCP, so other agents can use them.

Memory is not a log beside the graph. It hangs off the same nodes the agents already query:

(:Agent ) - [:WROTE ] -> (:Finding { text, validFromMs, validToMs }) (:Finding ) - [:ABOUT ] -> (:Station |:Route ) (:Finding ) - [:SUPERSEDES ] -> (:Finding ) Enter fullscreen mode Exit fullscreen mode Here are two moments where it changes what the agents do:

Because closed facts stay in the graph, the memory page can draw every fact over its validity window, next to incidents and agent steps.

I didn't want to trust a number just because a database produced it. A separate Python program built on NetworkX reads the same data files, follows the same written spec, and recomputes everything. FalkorDB and NetworkX agree on the max flow in all six direction groups of the three closures, and on the displaced-rider counts at every depth. The proof page shows them side by side.

Every visitor also gets their own sandbox graph, so I load-tested the multi-tenancy. 48 reader agents and 6 writers across 6 tenant graphs sustained 7,195 reads and 719 writes a second, with zero errors, and a closure in one tenant never showed up in another.

Next.js 16, FalkorDB 4.22 on FalkorDB Cloud, the Vercel AI SDK with free Gemini and Groq models, and MTA open data. I built Detour with Claude Code as a pair programmer, and it helped me draft this post. The graph model, the queries and every decision are documented in the repo.

Quelle: dev.to (dev.to). Original öffnen