Skip to content

[bug] The "Coder needs you" notification deep-links to a route that does not exist — the splat is parsed positionally #344

Closed

Description

@serge-ivo

The link points at a route that does not exist

codingDeepLink (workers/api/src/workflows/coding-session.ts:790) emits:

/console/instances/:id/coding/repos/:repoId/summary

The console routes instances/:id/* to InstanceDetail, which parses the splat positionally:

const { id, "*": splat } = useParams<{ id: string; "*": string }>();
const splatParts = splat?.split("/") || [];

So coding/repos/<repoId>/summary is read as tab coding with an initialSessionId of the literal string "repos". The repo id and summary are ignored entirely.

That link is on the Coder's "🙋 Coder needs you" notification — the one raised precisely when a run is blocked waiting for a human. So the notification that most needs to land somewhere useful lands on a session id that cannot exist.

Same class as #338, different producer

#338 (1e4c7f1) fixed the deploy notification by linking a stable parent — /console/instances/:id/coding?builds=<repoId> — rather than a per-run page the product does not have. The same reasoning applies here: link the thing that exists before, during and after the event.

While fixing #338 it also found the notification url column has been stored since migration 0026, returned by SELECT *, and never read by the in-app list — so clicking a notification did nothing at all. That is fixed, which means this broken URL is now reachable where before it was inert. Fixing the producer matters more than it did last week.

Worth checking while you are there

Every other deepLink-style producer in workers/api. Two have now been found wrong by inspection rather than by report, which suggests nothing validates them:

  • a link is a string built in a Worker,
  • the routes are declared in a React app,
  • nothing checks the two agree.

A guard is plausible: the console's route table is enumerable, and the set of link-producing helpers is small. That would make this class fail at build time instead of on a user's phone at the moment they are being asked for help. Judge whether it is worth it — a targeted fix plus a test may be enough.

Acceptance

Found by the agent that fixed #338, in a file outside its lane.

Activity

  1. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Fixed in b09fabf (CI green, API + Host deployed).

    Target: /console/instances/:id/coding/<sessionId> — the shape splatParts[1] was always built for. It satisfies #338's exists-before rule: the coding_sessions row is written by POST /coding/sessions before the workflow is dispatched, listSessions returns it whatever its status, and it outlives the run — so the link works before the Pilot blocks, while it waits, and hours later. It is also where the human actually answers: resolving a handoff is a message sent to that session. An id that no longer resolves falls back to the repo list rather than breaking.

    Second broken producer found by the sweep: the agent-loop's "Your agent needs you" pointed at /console/#/instances/<id> — a hash path on a BrowserRouter, so the tap opened the console and stopped wherever the user last was. Now instanceLink(). The other six (deployDeepLink, two board links, résumé knowledge/profile, trigger board, push test) were correct and now go through the same module.

    Guard built — judged worth it, since two were found by reading and none by report:

    • workers/api/src/lib/console-links.ts is the only place under workers/api/src that may build a console link.
    • console-links.test.ts calls every function that module exports (so a new builder is covered the moment it is written) and checks the result against the console's own route table, store/console/src/lib/routes.ts, imported rather than restated.
    • The check is not route-existence — that passes both bugs, because instances/:id/* matches any splat and path="*" swallows the rest. It encodes what the router does not police: the instance splat is <tab>/<sessionId>, the tab must be a real surface, a #fragment is never read, and a cross-origin URL cannot move an open tab.
    • A source scan forbids a /console/… literal anywhere else, so a producer cannot dodge the check by living elsewhere; routes.test.ts holds the table against App.tsx and surfaces.tsx so it cannot drift.

    Mutation proving it fires — pointed at the old string, before the fix:

    × codingSessionLink() lands on a page that exists
      → /console/instances/inst_1/coding/repos/id_2/summary: carries 4 splat segments but
        InstanceDetail parses only <tab>/<sessionId>; "id_2", "summary" is ignored
    

    Not changed: notification rows already in D1 keep the old URL; they route to the Coding tab's repo list rather than breaking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions