POST /api/agents/:id/runs previously awaited resolvedMonitor.executeHeartbeat
end-to-end before sending the response. For real provider runs that take
tens of seconds to minutes, Safari (and intermediate proxies) drop the
client socket and the dashboard surfaces "Failed to start heartbeat run:
load failed" — the run is actually in flight, but the toast suggests it
failed to start.
The route now kicks off executeHeartbeat in the background, polls briefly
for the active-run record (created synchronously inside executeHeartbeat
→ startRun), and returns 201 with that record. Synchronous failures of
executeHeartbeat are still surfaced to the client; background failures
are logged via runtimeLogger.child("heartbeat"). The 409 active-run
conflict contract is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dashboard API route registrars
packages/dashboard/src/routes.ts remains the single public entrypoint (createApiRoutes(store, options)), but route definitions are registered by domain modules in this directory.
Shared context contract
All registrars receive ApiRoutesContext from ./types.ts, built by createApiRoutesContext() in ./context.ts.
Registrars should be typed as ApiRouteRegistrar so modules share one explicit registration contract.
The context centralizes cross-cutting dependencies so registrars preserve behavior without re-implementing plumbing.
Some registrars (for example register-task-workflow-routes.ts) also take a narrow dependency-injection object for non-context helpers that must stay source-of-truth in routes.ts (git helpers, background refresh helpers, multer upload middleware). This avoids helper duplication while preserving runtime parity.
The context provides core cross-cutting plumbing:
- Request/project scoping:
getProjectIdFromRequest,getScopedStore,getProjectContext- These are also exported from
context.tsas canonical helpers for future extraction tasks.
- These are also exported from
- Engine-aware fallback behavior for project-bound and root-store APIs
- Runtime loggers and diagnostics emitters (
runtimeLogger,planningLogger,chatLogger) - Proxy/auth/audit helpers (
emitRemoteRouteDiagnostic,emitAuthSyncAuditLog) - Automation/routine resolvers and scope parsing helpers
- Shared error normalization (
rethrowAsApiError)
Registrar module map
register-settings-memory-routes.ts— settings APIs and memory backend/file/insight routes (excluding node-to-node sync endpoints)register-project-routes.ts—/projectsCRUD +/projects/across-nodes,/projects/detect, health/config/pause/resume routesregister-node-routes.ts—/nodesCRUD + operational endpoints (/health-check,/metrics,/version,/sync-plugins,/compatibility)register-settings-sync-routes.ts— node settings/auth sync routes (/nodes/:id/settings*,/nodes/:id/auth/sync)register-mesh-routes.ts— mesh topology routes (/mesh/state,/mesh/sync)register-discovery-routes.ts— discovery routes (/discovery/status|start|stop|nodes|connect) withoptions?.centralCorereuseregister-settings-sync-inbound-routes.ts— inbound sync/auth endpoints (/settings/sync-receive,/settings/auth-receive,/settings/auth-export)register-settings-sync-helpers.ts— shared sync-domain helpers (fetchFromRemoteNode,readStoredAuthProvidersFromDisk)register-task-workflow-routes.ts— task/workflow domain (/tasks*,/documents, task comments/docs/checkout/spec/attachments, PR+issue status, task lifecycle/workflow endpoints)register-planning-subtask-routes.ts— planning sessions and subtask breakdown routesregister-chat-routes.ts— chat session/list/mutation/stream routesregister-messaging-scripts.ts— scripts API and mailbox/message routesregister-git-github.ts— git/GitHub workflows and related helpers- Git plumbing routes:
/git/remotes*,/git/status,/git/commits*,/git/branches*,/git/worktrees,/git/fetch|pull|push,/git/stashes*,/git/diff*,/git/changes,/git/stage|unstage|commit|discard - GitHub import/integration routes:
/github/issues/*,/github/pulls/*,/github/webhooks,/github/batch/status(includes shared batch-import rate limiter state + reset export) - Task-scoped GitHub routes:
/tasks/:id/pr/*and/tasks/:id/issue/*status/refresh/create flows
- Git plumbing routes:
register-model-routes.ts—/modelsendpoint, favorites projection, anduseClaudeClifiltering forpi-claude-clientriesregister-auth-routes.ts— auth/provider domain (/auth/status,/auth/login,/auth/logout,/auth/api-key,/auth/claude-cli,/providers/claude-cli/status)register-usage-routes.ts—/usageendpoint withfetchAllProviderUsage(options?.authStorage)integrationregister-files-terminal-workspaces.ts— infrastructure aggregator for file/workspace + session-diff + terminal routes- Calls
register-session-diff-routes.tsfirst (session changed files + task diff endpoints) - Calls
register-file-workspace-routes.tssecond (task/workspace file browsing and file operations) - Calls
register-terminal-routes.tslast (terminal command/session + PTY lifecycle endpoints)
- Calls
register-file-workspace-routes.ts— task/workspace file domain:- Task files:
/tasks/:id/files,/tasks/:id/files/{*filepath}(read/write) - Workspace discovery/files:
/workspaces,/files,/files/markdown-list,/files/search,/files/{*filepath} - File operations:
/files/{*filepath}/copy|move|delete|rename,/files/{*filepath}/download,/files/{*filepath}/download-zip - Generic wildcard write:
/files/{*filepath}(must remain after operation routes) - Project markdown search:
/project-files/md
- Task files:
register-session-diff-routes.ts— task session/diff domain:- Session changed-file list:
/tasks/:id/session-files - Aggregate task diff:
/tasks/:id/diff - Per-file diffs:
/tasks/:id/file-diffs - Caches: module-level
sessionFilesCacheandfileDiffsCache(10-second TTL)
- Session changed-file list:
resolve-diff-base.ts— shared git diff-base utilities:runGitCommand(args, cwd, timeoutMs)resolveDiffBase(task, cwd)+ResolveDiffBaseTaskInputtype
register-terminal-routes.ts— terminal execution and PTY endpoints:- Command execution + streaming:
/terminal/exec,/terminal/sessions/:id,/terminal/sessions/:id/stream,/terminal/sessions/:id/kill - PTY lifecycle:
/terminal/sessions(create/list) and/terminal/sessions/:id(delete)
- Command execution + streaming:
register-agent-core-routes.ts— core agent CRUD, lookups, stats/org-tree, hierarchy aliases (/agents/:id/children|employees)register-agent-runtime-routes.ts— agent runtime/control-plane, heartbeats/runs, access/permissions, soul/memory, revisions/budget/keys, task/inbox surfacesregister-agent-reflection-rating-routes.ts— reflection/performance/context endpoints and ratings APIsregister-agent-import-export-generation-routes.ts— agent import/export, companies catalog, and/agents/generate/*session/spec lifecycleregister-agent-skills-routes.ts— skills discovery/content/execution/catalog endpoints coupled to agent capability flowregister-plugins-automation.ts— plugin CRUD, automation, routines/webhooksregister-proxy-routes.ts— remote-node proxy forwarding and SSE proxy routes- Injected dependencies:
{ store, runtimeLogger } - Endpoint inventory (must remain in this order):
GET /proxy/:nodeId/healthGET /proxy/:nodeId/projectsGET /proxy/:nodeId/tasksGET /proxy/:nodeId/project-healthGET /proxy/:nodeId/events(SSE pass-through, 30s timeout, client-disconnect cleanup)ALL /proxy/:nodeId/{*splat}(generic wildcard forwarder)
- Shared diagnostics: imports
emitRemoteRouteDiagnosticandclassifyRemoteRouteErrorfromroutes/context.tsso proxy and non-proxy registrars (for example mesh/sync routes) keep one diagnostic classification contract.
- Injected dependencies:
createApiRoutes mount sequence (current)
createApiRoutes() mounts registrars in this precedence-sensitive order:
registerSettingsMemoryRoutes(...)registerTaskWorkflowRoutes(...)registerPlanningSubtaskRoutes(...)registerChatRoutes(...)registerMessagingScriptRoutes(...)registerGitGitHubRoutes(...)registerFilesTerminalWorkspaceRoutes(...)registerAgentsProjectsNodesRoutes(...)registerPluginsAutomationRoutes(...)- (later)
registerAgentSkillsRoutes(...) - (last)
registerProxyRoutes(...)
Compatibility re-exports that must remain on routes.ts for tests and existing importers:
resolveDiffBase+ResolveDiffBaseTaskInput(fromresolve-diff-base.ts)__resetBatchImportRateLimiter(fromregister-git-github.ts)__setCreateFnAgentForRefine(defined inroutes.ts)
Ordering rules (critical)
Express matches in registration order. Keep registrar and in-registrar route ordering stable:
- Specific operation routes before generic parameterized routes (
/runs,/runs/:id,/copy,/deletebefore/:idstyle handlers) - Specific operation routes before wildcard paths (
/files/{*filepath}/copy|move|delete|rename|download|download-zipbeforePOST /files/{*filepath})- Why: Express route matching is first-win. If the wildcard write route is registered first, paths like
/files/somefolder/deletewill be treated as file writes instead of delete operations.
- Why: Express route matching is first-win. If the wildcard write route is registered first, paths like
- Do not move proxy/script/message/file wildcards ahead of specific routes
- For proxy routes specifically, keep all explicit
GET /proxy/:nodeId/*handlers ahead ofALL /proxy/:nodeId/{*splat}and keep proxy registration last increateApiRoutes().
- For proxy routes specifically, keep all explicit
- Project/node/sync/discovery ordering constraints must stay intact:
/projects/across-nodesand/projects/detectmust be registered before/projects/:id/nodes/:id/settingsmust be registered before/nodes/:id/settings/push|pull|sync-statusand before/nodes/:id/auth/sync/mesh/statemust be registered before/mesh/sync- Discovery routes stay grouped after mesh routes
- Inbound
/settings/sync-receive|auth-receive|auth-exportroutes mount after discovery routes
- Auth/model/usage ordering constraints must stay intact:
- Keep
/modelsregistration before auth-dependent picker/settings flows that rely on consistent model filtering - Keep auth registrar routes grouped as currently mounted (status/diagnostic + mutation endpoints) so no wildcard handler can shadow
/providers/claude-cli/status - Keep
/usagemounted as a standalone registrar route (not under auth paths) with unchanged error mapping semantics
- Keep
- Agent ordering constraints must stay intact:
/agents/stats,/agents/org-tree,/agents/resolve/:shortnamebefore/agents/:id/agents/:id/runs/stopbefore/agents/:id/runs/:runId/agents/:id/reflections/latestbefore/agents/:id/reflections
If adding a new endpoint, place it in the domain registrar and verify it does not shadow existing handlers.
Integrated routers
Integrated routers are mounted through register-integrated-routers.ts and intentionally called from routes.ts at precedence-sensitive points:
registerIntegratedRouters(...)mounts:createMissionRouter→/api/missionscreateRoadmapRouter→/api/roadmapscreateInsightsRouter→/api/insights
registerIntegratedDevServerRouter(...)mounts:createDevServerRouter→/api/dev-server
Keep these calls in their current positions inside createApiRoutes() unless an explicit route-ordering migration is planned and regression-tested.