From 1216e322b2396d001f2a2a697fa824d81c52f473 Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Tue, 28 Jul 2026 20:21:04 +0300 Subject: [PATCH] docs(server): name the route-order dependency at both ends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/api/kilds/archive` and `/api/kilds/status` work only because they are registered ABOVE `/api/kilds/:id`. Nothing said so. Verified rather than assumed, because the assumption could have gone either way — some routers prefer a static segment over a param regardless of order. Hono does not. Registered `:id` first in a throwaway app and asked for `/api/kilds/status`: it matched the param route with `id="status"`. The dependency is real. The failure it would produce is the expensive kind: resolveKild answers "no such kild" and returns 404, so the symptom is a missing kild rather than a swallowed route, and `/api/kilds/status` is what the entire cheap/costly polling split rests on. e2e already covers it — a reorder breaks the archive and status assertions — but it breaks them as "endedAt is present" failing, which names neither the cause nor the fix. A comment at the point where someone would actually move the line costs nothing and turns a confusing CI failure into a mistake not made. Noted at both ends, on the same reasoning as the branchKept dependency: a comment saying "something depends on this" is only useful if it names the thing. Each site points at the other. Raised by helm's agent from the outside, as a latent edge they could see and could not test. It was worth testing. Gates: 467 tests, typecheck, lint, e2e 70/70. --- engine/src/server.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/engine/src/server.ts b/engine/src/server.ts index 685cc5e3..df89aed8 100644 --- a/engine/src/server.ts +++ b/engine/src/server.ts @@ -305,6 +305,11 @@ app.get('/api/agents', (c) => c.json(agentManager.list())); // The archive is a listing, so it carries no log — `GET /api/kilds/:id/messages` serves an // archived kild's log exactly as it serves a live one. The archive only grows, so shipping // every stopped kild's full conversation here was the most expensive listing in the engine. +// ORDER-DEPENDENT: this must stay ABOVE `/api/kilds/:id`. Hono matches in REGISTRATION +// order, not static-before-param — verified: with `:id` registered first, a GET for +// `/api/kilds/archive` binds `id="archive"` and this handler never runs. The failure is a +// 404 from resolveKild, which reads as "no such kild" rather than as a routing mistake. +// Same applies to `/api/kilds/status` below. app.get('/api/kilds/archive', (c) => c.json(kildManager.archivedViews())); // ── The kild collection ─────────────────────────────────────────────────────── @@ -433,6 +438,8 @@ app.get('/api/kilds', async (c) => { // uncommittedFiles, changedFiles, conflictsWithBase) plus cost rollups and per-agent // tokens/cost. Still no logs — those are `/api/kilds/:id/messages`. // `?state=live|orphan|reclaimable` filters; `?project=`/`?path=` scopes to one repo. +// ORDER-DEPENDENT: must stay ABOVE `/api/kilds/:id` — see the note on `/api/kilds/archive`. +// This is the route the whole cheap/costly polling split rests on, so it fails expensively. app.get('/api/kilds/status', async (c) => { const scoped = await kildScope(c); if (!scoped.ok) return c.json({ error: scoped.error }, scoped.status); @@ -451,6 +458,10 @@ app.get('/api/kilds/status', async (c) => { // ONE kild, with its git state and cost — and WITHOUT its log, which is its own cursored // resource. Bounded cost by construction: one kild's git, never every kild's. +// Registered AFTER the static `/api/kilds/archive` and `/api/kilds/status` deliberately. +// Hono matches in registration order, so moving this above either one silently swallows it: +// the id binds to the literal word and resolveKild answers "no such kild". Nothing about the +// resulting 404 says the cause was routing. app.get('/api/kilds/:id', async (c) => { const id = c.req.param('id'); const live = await kildManager.liveKildStatus(id);