Skip to content

Consolidate duplicate and dead HTTP routes serving the same session resource #56

Description

@moshloop

The HTTP surface serves the same session resource from several parallel routes with different handlers and different JSON shapes, and several registered routes have no caller at all. Found while fixing the session detail page dropping its changed files (sessionFromOverview never mapped metadata->'files'); these are the sibling duplications the same audit surfaced.

Findings, in severity order

1. GET /api/captain/sessions/{id} is dead and duplicates GET /api/v1/sessions/{id}.
Both resolve to cli.RunSessionGet (pkg/cli/session_get.go:50) and emit SessionGetResult. The webapp uses only the /api/v1 one (pkg/cli/webapp/src/sessionData.ts:353); the /api/captain twin is referenced only by pkg/cli/serve_test.go:98. Removal is not purely mechanical — POST /api/captain/sessions/{id}/message shares the prefix.

2. Three envelopes for one session aggregate.

  • GET /api/v1/sessions/{id}{sessions:[{summary, detail, activeRunId, chat, chatState}]}
  • GET /api/chat/sessions/{id} → a bare *session.Session (pkg/aichat/threads_http.go:98)
  • GET /api/chat/sessions[]*Thread, a third projection of the same rows

A session opened from /chat/{id} is rendered from a payload with no chat/chatState/activeRunId, so live-run affordances are unavailable there.

3. /api/captain/projects vs /api/v1/projects are the same noun, different everything.
RunProjectOptions (pkg/cli/projects.go:76) is a DB aggregate returning {value,label,path,sources,sessions,lastUsed}; RunProjectsList (projects.go:53) is a filesystem scan returning {project,sessions,size,lastUsed} with pre-formatted display strings. The project picker gets the first, the chat agent gets the second. They will disagree.

4. Two independent cancel paths for one run id.
task.WithGroupID(runID) (pkg/cli/prompt_run.go:105) makes the clicky task group id equal the prompt run id, so POST /api/captain/tasks/{id}/control and POST /api/captain/prompt/runs/{id}/stop (prompt_chat_http.go:53) both cancel the same run through unrelated machinery, and GET /api/captain/tasks/{id} and GET /api/captain/prompt/runs/{id} describe it with unrelated JSON. Nothing reconciles them.

5. captain plan with no argument can never return a native plan.
resolveLatestTranscriptPlan (pkg/cli/plan.go:88) skips resolveNativePlan (:187), so it shows the stale transcript copy while captain plan <id> on the same session shows the approved revision from captain_plans. (The session detail path now prefers the native plan; captain plan still does not.)

6. Unmatched /api/** returns index.html with 200, which is why dead routes go unnoticed.
pkg/cli/serve.go:205 falls through to the UI handler, and newCaptainWebappHandler (serve.go:378-393) serves index.html for any path whose last segment has no dot. A wrong API path returns HTML rather than 404/405. Concretely: clicky-ui's TaskManager.tsx:301 POSTs /api/captain/tasks/{id}/tasks/{taskId}/control, which task.RegisterHandlers never registers, and silently receives HTML.

7. Other dead routes. GET /api/chat/tools (the UI injects tools so ChatWindow never fetches it), POST /api/chat/sessions/{id}/interrupt, GET /api/entities, POST /api/v1/ai/models.

8. Duplicated datemath parsing. parseSessionQueryTime (pkg/cli/serve_sessions.go:83) reimplements what clicky's flag parser already does, so the two session-list routes have two parsers and two error bodies for the same bad from=.

Note on why the /api/captain/* twins exist

Clicky's executor infers the HTTP method from the command path and defaults to POST, so read-only commands (sessions live, contexts, cost, plan, whoami) are exposed as POST. DatabaseContextMiddleware (pkg/cli/db_context_http.go:55) rejects non-GET on a non-default context with 409. The hand-written GET /api/captain/{contexts,sessions/live,projects} routes exist to work around that. The duplication is a symptom of the method inference, not an independent design — fixing it at that seam may remove several twins at once.

Scope

Triage and split as needed. Items 1, 6 and 7 are cheap deletions/guards. Items 2-4 need a decision about which contract wins. Item 5 is a one-line resolver-order fix.

Acceptance criteria

  • Every route registered under /api/** either has a caller or is removed.
  • An unmatched /api/** path returns 404, not index.html with 200.
  • One session-aggregate contract, or an explicit documented reason for each variant.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions