HTTP routes
Generated from the route constants and dispatcher in packages/web/src/server/index.ts, with handler contracts from server/follow.ts, server/bundle-route.ts, and server/logs-route.ts.
Every /api/threads/:threadId… route also reads the X-Enso-Project header (#395): a page names a new thread’s project with it, and an id that is no registered project is a 400 before the route runs.
Parser limitation: discovery recognizes top-level
const *_ROUTE = /…/,const *_ROUTE = threadRoute(…),const *_ROUTE = promptRoute()andconst *_ROUTE = new RegExp(declarations (onlythreadRoutederives its path; every other form states it in its metadata) plus directrequest.methodbranches usingurl === …orurl.startsWith(…). Statuses, input shapes, classes, acquisition, and self-logging are intentionally explicit checked metadata rather than inferred behavior.
| Method | Rendered path | Handler / factory | Input shape | Success | Named error statuses | Class | Builds / acquires? | Logs itself? | Source |
|---|---|---|---|---|---|---|---|---|---|
| GET | /api/about |
createAboutRoute → about |
none | 200 EnsoBundleAbout JSON |
403 untrusted origin | observation | no | no | packages/web/src/server/index.ts:1542packages/web/src/server/bundle-route.ts:169 |
| POST | /api/logs |
respondToBrowserLogs → receiveBrowserLogs |
body: EnsoBrowserLogBatch |
204 empty | 400 malformed batch; 403 untrusted origin; 413 body too large; 500 body-read failure | browser-log-ingest | no | no | packages/web/src/server/index.ts:1564packages/web/src/server/logs-route.ts:29 |
| GET | /api/logs/bundle* |
createBundleRoute → bundle |
query: optional thread id/prefix |
200 Markdown attachment | 400 malformed thread filter; 403 untrusted origin | observation | no | no | packages/web/src/server/index.ts:1549packages/web/src/server/bundle-route.ts:98 |
| GET | /api/logs/stats |
createLogStatsRoute → stats |
none | 200 EnsoDayStats JSON |
403 untrusted origin | observation | no | no | packages/web/src/server/index.ts:1558packages/web/src/server/logs-stats-route.ts:62 |
| GET | /api/logs/tail* |
followLogTail |
query: optional level, thread id/prefix, process, since duration, from byte offset with the day it belongs to |
200 SSE stream of EnsoLogTailFrame; a read the server cannot do ends it with an error event |
400 unknown level, malformed thread filter, malformed since or from; 403 untrusted origin | observation | no | no | packages/web/src/server/index.ts:1553packages/web/src/server/logs-tail-route.ts:152 |
| GET | /api/mode |
respondWithFreshThreadMode |
none | 200 permission-mode JSON | 403 untrusted origin | static | no | no | packages/web/src/server/index.ts:1530 |
| GET | /api/projects |
respondWithProjects |
none | 200 EnsoProject[] JSON — the launch project first, then the registered ones |
403 untrusted origin | observation | no | no | packages/web/src/server/index.ts:1333 |
| POST | /api/projects |
respondToProjectRegistration → ProjectRegistry.register |
body: EnsoProjectRegistration — an absolute path strictly under a root the ENSO_HOME config declares (projects.roots), optional title |
201 EnsoProject JSON when registered; 200 when the directory already is a project |
400 malformed body; 403 untrusted origin; 413 body too large; 422 refused, with the reason — not absolute, missing, not a directory, a root itself, outside every root (resolved through symlinks), or inside a hidden directory; 500 handler failure | control | no | yes | packages/web/src/server/index.ts:1337 |
| DELETE | /api/projects/:projectId |
respondToProjectRemoval → ProjectRegistry.remove |
path: projectId (sixteen hex digits) |
204 empty — the registration is forgotten; its sessions stay on disk and are served again if the directory is registered again | 403 untrusted origin; 404 no registered project with that id; 409 the launch project, or a thread in the project is running | control | no | yes | packages/web/src/server/index.ts:140 |
| GET | /api/projects/browse* |
respondWithBrowse → ProjectRegistry.browse |
query: optional path — a directory inside a declared root; absent, the roots themselves |
200 EnsoDirectoryListing JSON — the directories under it that are inside a root and not hidden (symlinks resolved), which already are projects, and whether this one may be added |
403 untrusted origin; 422 refused, with the reason — not absolute, missing, not a directory, outside every root, or inside a hidden directory | observation | no | no | packages/web/src/server/index.ts:1341 |
| GET | /api/prompts |
respondWithPrompts → PromptLibrary.list |
none | 200 EnsoLibraryPromptListing JSON — the library’s prompts by name without their bodies, each with its bytes and estimated tokens, and every .md file it refused, with the reason naming it |
403 untrusted origin; 404 a server started without a library; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:1456 |
| DELETE | /api/prompts/:id |
respondToPromptRemoval → PromptLibrary.remove |
path: id; header: If-Match: <ETag> (the version read) or If-Match: * |
204 empty — the file is gone; a thread that ran it keeps the text its log recorded | 400 If-None-Match, a list, or a weak tag; 403 untrusted origin; 404 no such file, or a server started without a library; 412 the file changed on disk since it was read; 428 no If-Match; 500 handler failure |
control | no | yes | packages/web/src/server/index.ts:138 |
| GET | /api/prompts/:id |
respondWithPrompt → PromptLibrary.read |
path: id — the file name under ENSO_HOME/prompts without .md, lowercase words joined by hyphens |
200 EnsoLibraryPrompt JSON — its frontmatter fields, its body, the file’s bytes and the body’s estimated tokens — with an ETag header, the sha256 of the file’s bytes, which a later replace or delete of this version names |
403 untrusted origin; 404 no such file, or a server started without a library; 422 the file is refused, with the reason naming it — unreadable, not UTF-8, no frontmatter, an unknown key, no name, a category that is not system, append or session; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:136 |
| PUT | /api/prompts/:id |
respondToPromptWrite → PromptLibrary.write |
path: id; headers: exactly one precondition — If-None-Match: * (create: no file of that name), If-Match: <ETag> (replace the version read), If-Match: * (replace whatever is there); body: EnsoLibraryPromptWrite — name, description, category (system, append or session) and the markdown body |
200 EnsoLibraryPrompt JSON with its new ETag — the file as read back after it was written whole (staged, then linked into place for a create, renamed over for a replace) |
400 malformed body, or both headers, a list, or a weak tag; 403 untrusted origin; 404 a server started without a library; 412 the precondition does not hold, with the reason naming the file — it already exists (a refused file included), changed on disk since it was read, or is gone; 413 body too large; 428 no precondition — never an unconditional write; 500 the file does not read back, or a disk failure | control | no | yes | packages/web/src/server/index.ts:137 |
| GET | /api/stored-threads |
respondWithStoredThreads |
none | 200 EnsoStoredThread[] JSON |
403 untrusted origin; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:1538 |
| GET | /api/threads |
respondWithThreads |
none | 200 EnsoThreadSummary[] JSON |
403 untrusted origin | observation | no | no | packages/web/src/server/index.ts:1534 |
| GET | /api/threads/:threadId |
respondWithThread |
path: threadId |
200 JSON | 400 malformed thread id; 403 untrusted origin; 404 no live session | observation | no | no | packages/web/src/server/index.ts:81 |
| POST | /api/threads/:threadId/abort |
respondToAbort → abortThread |
path: threadId; no body |
200 EnsoAbortReceipt JSON |
400 malformed thread id; 403 untrusted origin; 404 no live session; 409 nothing running | control | no | yes | packages/web/src/server/index.ts:106packages/web/src/server/follow.ts:895 |
| GET | /api/threads/:threadId/changes |
respondWithThreadChanges |
path: threadId |
200 EnsoThreadChanges JSON — git’s state in the thread’s cwd and every file that differs from its last-push baseline (#452): unpushed commits, staged and unstaged edits, deletions, renames and untracked files, each with its unified diff unless binary or too large; truncated past 500 files. A diff git cannot produce reads as unavailable. Read-only, no fetch |
400 malformed thread id; 403 untrusted origin; 404 no session, or its project is no longer served; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:101 |
| POST | /api/threads/:threadId/command |
respondToHostCommand → runHostCommand |
path: threadId; body: EnsoHostCommandRun — model with provider/id, thinking with a level, login with provider/authType, logout with a provider, compact with nothing; a view row (context, fork) is the page’s to do, never run |
204 empty for a setting (the change arrives on the follow as model); 202 empty for a flow or action (login, logout, compact) — started, its questions and progress arrive on the follow as dialogs and notify lines |
400 malformed id/body; 403 untrusted origin; 409 thread busy; 413 body too large; 422 unknown command, model or level, a provider without credentials, or a view (/context, /fork) the page does itself; 500 handler failure |
control | yes | yes | packages/web/src/server/index.ts:110 |
| GET | /api/threads/:threadId/commands |
respondWithThreadCommands |
path: threadId |
200 EnsoThreadCommands JSON — source says live, remembered or none |
400 malformed thread id; 403 untrusted origin | observation | no | no | packages/web/src/server/index.ts:87 |
| POST | /api/threads/:threadId/configure |
respondToConfigure → configureThread |
path: threadId; body: EnsoSessionConfiguration — optional systemPrompt (a library prompt id, system or append, or null for pi’s default; absent leaves the session’s as it is), model (provider/id), thinking (a level that model supports) and mode; applied in that order under one lease, a value equal to the current one left alone |
204 empty — every step applied (the changes arrive on the follow as model and permission-mode) |
400 malformed id/body; 403 untrusted origin; 409 thread busy; 413 body too large; 422 JSON EnsoConfigurationRefusal — the step that refused (systemPrompt, model, thinking, mode) and the reason, the steps before it applied — a system prompt is refused by name when its id is not a library name, its file is missing, unreadable or unparseable, a session prompt, or empty, or the session already sent a request (before it, a new choice — pi’s default included — replaces the last); 500 handler failure |
control | yes | yes | packages/web/src/server/index.ts:112 |
| GET | /api/threads/:threadId/context |
respondWithThreadContext |
path: threadId |
200 EnsoThreadContext JSON — pi’s branch, request by request, and its context meter |
400 malformed thread id; 403 untrusted origin; 404 no session; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:91 |
| GET | /api/threads/:threadId/context/:request |
respondWithContextRequest |
path: threadId, request (a turn number or next) |
200 EnsoContextRequestDetail JSON — one request’s context, element by element |
400 malformed thread id; 403 untrusted origin; 404 no session or no such turn; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:93 |
| POST | /api/threads/:threadId/continue |
respondToContinue → continueThread → ThreadRuntime.continueRun |
path: threadId; no body |
202 empty — the torn tail is continued from the stored state with no new user message; the run arrives on the follow and its settled releases the thread |
400 malformed thread id; 403 untrusted origin; 404 no thread under that id, live or stored; 409 thread busy; 422 nothing to continue — the thread does not end on an unanswered prompt, an unfinished tool call, or tool results with no reply, or this pi build has no run lifecycle to continue in; 500 handler failure | control | yes | yes | packages/web/src/server/index.ts:127packages/web/src/server/follow.ts:440 |
| POST | /api/threads/:threadId/dialog |
respondToDialog → answerThreadDialog |
path: threadId; body: DialogAnswer |
204 empty | 400 malformed id/body; 403 untrusted origin; 404 no live question; 413 body too large; 422 answer refused; 500 handler failure | control | no | yes | packages/web/src/server/index.ts:105packages/web/src/server/follow.ts:518 |
| GET | /api/threads/:threadId/eval |
respondWithThreadEval |
path: threadId |
200 EnsoSessionEval JSON — the mechanical timeline and scorecard (#441): the branch’s events off pi’s entries, the queued prompts and interrupts off the day files the branch spans |
400 malformed thread id; 403 untrusted origin; 404 no session; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:97 |
| GET | /api/threads/:threadId/events |
respondWithThreadEvents |
path: threadId |
200 EnsoThreadEvents JSON |
400 malformed thread id; 403 untrusted origin; 404 no live session | observation | no | no | packages/web/src/server/index.ts:83 |
| GET | /api/threads/:threadId/follow |
followThread |
path: threadId |
200 SSE stream | 400 malformed thread id; 403 untrusted origin; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:103packages/web/src/server/follow.ts:202 |
| POST | /api/threads/:threadId/fork |
respondToFork → forkThreadAt → AgentHost.forkThread |
path: threadId; body: EnsoForkRequest — entryId, the pi entry id of one of the thread’s user messages; the fork is pi’s /fork at position before, taken from the session file |
200 EnsoForkReceipt JSON — the new thread id (its session file names the source as parentSession) and the forked message’s text, for the new composer |
400 malformed id/body, or an entry that is no user message; 403 untrusted origin; 404 no session file in a served project, or no such entry; 409 thread busy; 413 body too large; 500 handler failure | control | no | yes | packages/web/src/server/index.ts:125packages/web/src/server/follow.ts:851 |
| GET | /api/threads/:threadId/git |
respondWithThreadGit |
path: threadId |
200 EnsoThreadGit JSON — the thread’s cwd and git’s state there (#453): repository, branch, upstream, the last-push baseline and the rule that chose it, ahead/behind, and changed files by where they sit; not-a-repository or unavailable with git’s reason otherwise. Read-only, no fetch |
400 malformed thread id; 403 untrusted origin; 404 no session, or its project is no longer served; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:99 |
| GET | /api/threads/:threadId/history |
respondWithThreadHistory |
path: threadId |
200 EnsoThreadHistory JSON |
400 malformed thread id; 403 untrusted origin, or a session stored in a directory that is no project here (named, #378); 404 no session — an id nobody has used, which a page starts; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:85 |
| GET | /api/threads/:threadId/manifest |
respondWithThreadManifest |
path: threadId |
200 EnsoForkPoint JSON — identity, then run conditions, off pi’s branch |
400 malformed thread id; 403 untrusted origin; 404 no session; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:95 |
| POST | /api/threads/:threadId/mode |
respondToModeChange → changeThreadMode |
path: threadId; body: EnsoPermissionModeChange |
204 empty; the change arrives on the follow as permission-mode |
400 malformed id/body; 403 untrusted origin; 409 thread busy; 413 body too large; 422 mode not offered; 500 handler failure | control | yes | yes | packages/web/src/server/index.ts:108packages/web/src/server/follow.ts:551 |
| POST | /api/threads/:threadId/prompt |
respondToPrompt → promptThread |
path: threadId; body: EnsoPromptBody |
202 EnsoPromptReceipt JSON |
400 malformed id/body or unavailable command; 403 untrusted origin; 409 the thread was taken and released again while the prompt waited on its build (rare; #444); 413 body too large; 500 handler failure | control | yes | yes | packages/web/src/server/index.ts:104packages/web/src/server/follow.ts:340 |
| GET | /api/threads/:threadId/stats |
respondWithThreadStats |
path: threadId |
200 EnsoThreadStats JSON — pi’s session for the numbers, the log’s day files for the times |
400 malformed thread id; 403 untrusted origin; 404 no session; 500 handler failure | observation | no | no | packages/web/src/server/index.ts:89 |
| GET | /api/threads/:threadId/system-prompt |
respondWithSystemPromptPreview → HostedThread.previewSystemPrompt |
path: threadId; query: optional prompt — a library prompt id (system or append); absent is pi’s default |
200 EnsoSystemPromptPreview JSON — pi’s rendering of the next request’s system prompt with that prompt, section by section (who wrote each: the library or pi), the tool definitions sent beside it, and the total the Context tab prices a first request made with it at; the thread’s own choice is untouched |
400 malformed thread id; 403 untrusted origin; 404 a prompt named on a server without a library; 409 thread running; 422 the prompt is refused, with the reason naming it — missing, unreadable, not UTF-8 or unparseable, a session prompt, or empty; 500 pi’s rendering does not split back into its sections (the prompt’s own text is matched whole, so it cannot cause this), or handler failure | observation | yes | no | packages/web/src/server/index.ts:119 |
| POST | /api/threads/:threadId/system-prompt |
respondWithDraftPreview → HostedThread.previewSystemPrompt |
path: threadId; body: EnsoSystemPromptDraft — an unsaved prompt’s mode (system or append), text and name |
200 EnsoSystemPromptPreview JSON — the same preview a saved prompt gets, for the editor’s breakdown while it types; nothing is recorded |
400 malformed id/body; 403 untrusted origin; 409 thread running; 413 body too large; 500 pi’s rendering does not split back into its sections (the draft’s text is matched whole, so it cannot cause this), or handler failure | observation | yes | no | packages/web/src/server/index.ts:123 |
Detected 26 regex route constants and 12 literal dispatcher routes; metadata is exhaustive for both sets.
