/** * @file Enriched OVERRIDE fragments for the already-documented Sessions and * Agents endpoints. These paths exist in the base spec (server/openapi.js); the * loader (server/openapi-extra.js) merges `paths` with override-on-key * semantics, so the operations below REPLACE the terser base versions while * preserving their contract: same `operationId`, same `tags`, and the same * request/response `$ref` schema names. The only additions are richer * `description`s and realistic `example`s on every parameter, response media * type, and request body — purely documentation, no contract change. * * No new schemas are defined here (`schemas` is empty by design); everything * reuses the base `components.schemas` and `components.parameters`. Error * responses keep referencing the base `ErrorResponse` ({ error: { code, * message } }). The Sessions/Agents tags are already declared in the base * literal, so `tags` is intentionally empty. * * Covers: * - GET /api/sessions (listSessions) * - POST /api/sessions (createSession) * - GET /api/sessions/{id} (getSession) * - PATCH /api/sessions/{id} (updateSession) * - GET /api/sessions/{id}/stats (getSessionStats) * - GET /api/sessions/{id}/transcripts (listSessionTranscripts) * - GET /api/sessions/{id}/transcript (getSessionTranscript) * - GET /api/agents (listAgents) * - POST /api/agents (createAgent) * - GET /api/agents/{id} (getAgent) * - PATCH /api/agents/{id} (updateAgent) * * @author Nguyễn Ngọc Trí Vĩ */ const tags = []; const schemas = {}; // --- Reusable realistic example fixtures ---------------------------------- // Keep these consistent with the route handlers in server/routes/sessions.js // and server/routes/agents.js. Timestamps are ISO-8601 UTC with millisecond // precision; metadata is a raw JSON-encoded string (the DB column is TEXT). const exampleSession = { id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", name: "Refactor pricing route + add cost endpoint", status: "active", cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor", model: "claude-opus-4-20250514", started_at: "2026-06-25T14:02:11.004Z", ended_at: null, metadata: '{"source":"hook","git_branch":"feat/spend-budgets"}', updated_at: "2026-06-25T14:31:50.119Z", agent_count: 4, last_activity: "2026-06-25T14:31:50.119Z", cost: 0.8421, awaiting_input_since: null, awaiting_reason: null, }; const exampleCompletedSession = { id: "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9", name: "Fix flaky transcript pagination test", status: "completed", cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor", model: "claude-sonnet-4-20250514", started_at: "2026-06-24T09:12:00.000Z", ended_at: "2026-06-24T09:48:32.501Z", metadata: null, updated_at: "2026-06-24T09:48:32.501Z", agent_count: 1, last_activity: "2026-06-24T09:48:32.501Z", cost: 0.1532, awaiting_input_since: null, awaiting_reason: null, }; const exampleMainAgent = { id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main", session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", name: "Main Agent", type: "main", subagent_type: null, status: "working", task: null, current_tool: "Edit", started_at: "2026-06-25T14:02:11.004Z", ended_at: null, parent_agent_id: null, metadata: '{"model":"claude-opus-4-20250514"}', updated_at: "2026-06-25T14:31:50.119Z", awaiting_input_since: null, awaiting_reason: null, }; const exampleSubagent = { id: "ad18a79192af10ed1", session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", name: "Explore pricing module", type: "subagent", subagent_type: "Explore", status: "completed", task: "Map every caller of calculateCost() across server/routes", current_tool: null, started_at: "2026-06-25T14:10:22.310Z", ended_at: "2026-06-25T14:14:09.882Z", parent_agent_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main", metadata: null, updated_at: "2026-06-25T14:14:09.882Z", awaiting_input_since: null, awaiting_reason: null, }; const exampleEvent = { id: 48213, session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", agent_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main", event_type: "PostToolUse", tool_name: "Edit", summary: "Edited server/routes/pricing.js", data: '{"tool_input":{"file_path":"server/routes/pricing.js"},"tool_response":{"success":true}}', created_at: "2026-06-25T14:31:50.119Z", }; const paths = { "/api/sessions": { get: { tags: ["Sessions"], summary: "List sessions", description: "Returns a paginated list of sessions, newest activity first, each enriched with a SQL `agent_count` (LEFT JOIN onto agents), a `last_activity` alias of `updated_at`, and a `cost` computed from the session's token usage against the current pricing rules. The `status` and `q` filters compose (AND) with each other and with pagination; `q` is a case-insensitive LIKE across `id`, `name`, and `cwd`. `total` reflects all rows matching the filters independent of `limit`/`offset` so paginators stay accurate, while `cost` is only calculated for the rows on the returned page (when `sort_by=price` it is computed across all matching rows so the price sort is correct). The endpoint is read-only with no side effects; `metadata` on each session is returned as a raw JSON-encoded string, not a parsed object.", operationId: "listSessions", parameters: [ { $ref: "#/components/parameters/SessionStatusQuery", example: "active" }, { name: "q", in: "query", schema: { type: "string" }, description: "Case-insensitive search across `id` / `name` / `cwd`. Composes with the status filter when both are present.", example: "pricing", }, { $ref: "#/components/parameters/SourcesQuery", example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11", }, { $ref: "#/components/parameters/LimitQuery", example: 50 }, { $ref: "#/components/parameters/OffsetQuery", example: 0 }, ], responses: { 200: { description: "Session list", content: { "application/json": { schema: { $ref: "#/components/schemas/SessionsListResponse" }, example: { sessions: [exampleSession, exampleCompletedSession], limit: 50, offset: 0, total: 137, }, }, }, }, }, }, post: { tags: ["Sessions"], summary: "Create session (idempotent)", description: 'Creates a session keyed by `id`. The operation is idempotent: if a session with that `id` already exists it is returned untouched with `created: false` and HTTP 200; only a brand-new row yields `created: true` and HTTP 201. New sessions are inserted with `status: "active"` and any omitted optional fields stored as null. The `metadata` field is accepted as a JSON object in the request but persisted (and returned on the session) as a JSON-encoded string. A successful create broadcasts a `session_created` websocket frame. A missing `id` returns 400 with code `INVALID_INPUT`.', operationId: "createSession", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/SessionCreateRequest" }, example: { id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", name: "Refactor pricing route + add cost endpoint", cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor", model: "claude-opus-4-20250514", metadata: { source: "hook", git_branch: "feat/spend-budgets" }, }, }, }, }, responses: { 201: { description: "Session created", content: { "application/json": { schema: { $ref: "#/components/schemas/SessionCreateResponse" }, example: { session: { ...exampleSession, agent_count: 0, cost: 0, last_activity: "2026-06-25T14:02:11.004Z", updated_at: "2026-06-25T14:02:11.004Z", }, created: true, }, }, }, }, 200: { description: "Session already exists", content: { "application/json": { schema: { $ref: "#/components/schemas/SessionCreateResponse" }, example: { session: exampleSession, created: false, }, }, }, }, 400: { description: "Invalid request body", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "INVALID_INPUT", message: "id is required" } }, }, }, }, }, }, }, "/api/sessions/{id}": { get: { tags: ["Sessions"], summary: "Get session details", description: "Returns a single session together with all of its agents (chronological) and persisted events. Read-only, no side effects. The session's `metadata` and each event's `data` are returned as raw JSON-encoded strings, not parsed objects. Returns 404 with code `NOT_FOUND` when no session matches the path `id`.", operationId: "getSession", parameters: [ { $ref: "#/components/parameters/SessionIdPath", example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", }, ], responses: { 200: { description: "Session with associated agents/events", content: { "application/json": { schema: { $ref: "#/components/schemas/SessionDetailResponse" }, example: { session: exampleSession, agents: [exampleMainAgent, exampleSubagent], events: [exampleEvent], }, }, }, }, 404: { description: "Session not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "NOT_FOUND", message: "Session not found" } }, }, }, }, }, }, patch: { tags: ["Sessions"], summary: "Update session", description: "Partially updates a session by `id`. Only `name`, `status`, `ended_at`, and `metadata` are accepted; any field omitted from the body is passed as null and the underlying UPDATE uses COALESCE, so a null leaves the existing column value unchanged (partial-update semantics) — you cannot clear a field to null through this endpoint. `metadata` is supplied as a JSON object but stored and returned as a JSON-encoded string. A successful update re-reads the row and broadcasts a `session_updated` websocket frame. Returns 404 with code `NOT_FOUND` when the session does not exist.", operationId: "updateSession", parameters: [ { $ref: "#/components/parameters/SessionIdPath", example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", }, ], requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/SessionUpdateRequest" }, example: { status: "completed", ended_at: "2026-06-25T15:07:44.220Z", metadata: { source: "hook", git_branch: "feat/spend-budgets", outcome: "merged" }, }, }, }, }, responses: { 200: { description: "Session updated", content: { "application/json": { schema: { $ref: "#/components/schemas/SessionUpdateResponse" }, example: { session: { ...exampleSession, status: "completed", ended_at: "2026-06-25T15:07:44.220Z", metadata: '{"source":"hook","git_branch":"feat/spend-budgets","outcome":"merged"}', updated_at: "2026-06-25T15:07:44.220Z", last_activity: "2026-06-25T15:07:44.220Z", }, }, }, }, }, 404: { description: "Session not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "NOT_FOUND", message: "Session not found" } }, }, }, }, }, }, }, "/api/sessions/{id}/stats": { get: { tags: ["Sessions"], summary: "Get aggregated session stats", description: "Returns aggregated counts for the SessionOverview panel: total events, events-by-type, the top 15 tools by usage, an error count (events whose `event_type`/`summary` match /error/i or /failed/i), the event time range, agent type/status counts, the subagent-type breakdown (excluding the special `compaction` type, which is surfaced under `agents.compaction`), and token totals. All aggregation runs in SQL, so it stays cheap even for sessions with tens of thousands of events; the endpoint is read-only with no side effects. The frontend debounces calls on `new_event` / `agent_*` / `session_updated` websocket frames so the counters track a running session. Returns 404 with code `NOT_FOUND` when the session does not exist.", operationId: "getSessionStats", parameters: [ { $ref: "#/components/parameters/SessionIdPath", example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", }, ], responses: { 200: { description: "Aggregated session stats", content: { "application/json": { schema: { $ref: "#/components/schemas/SessionStatsResponse" }, example: { session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", total_events: 1284, events_by_type: [ { event_type: "PostToolUse", count: 612 }, { event_type: "PreToolUse", count: 612 }, { event_type: "Notification", count: 41 }, { event_type: "Stop", count: 19 }, ], tools_used: [ { tool_name: "Bash", count: 188 }, { tool_name: "Edit", count: 143 }, { tool_name: "Read", count: 121 }, { tool_name: "Grep", count: 77 }, ], error_count: 6, first_event_at: "2026-06-25T14:02:11.052Z", last_event_at: "2026-06-25T14:31:50.119Z", agents: { total: 4, main: 1, subagent: 3, compaction: 1, by_status: { working: 1, completed: 2, error: 1 }, }, subagent_types: [ { subagent_type: "Explore", count: 2 }, { subagent_type: "general-purpose", count: 1 }, ], tokens: { input_tokens: 18422, output_tokens: 9134, cache_read_tokens: 1204880, cache_write_tokens: 88210, }, }, }, }, }, 404: { description: "Session not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "NOT_FOUND", message: "Session not found" } }, }, }, }, }, }, }, "/api/sessions/{id}/transcripts": { get: { tags: ["Sessions"], summary: "List available transcripts for a session", description: "Lists every JSONL transcript file associated with a session — the main agent's transcript plus any subagent and compaction transcripts — by scanning the on-disk Claude project directory (live files, falling back to import-time snapshots). Read-only, no side effects. Each entry carries a best-effort `db_agent_id` resolved by matching transcripts to tracked agents (exact id first, then positional-by-time within each type group); it may be null when a transcript has no matching agent row. Used by the Conversation tab to populate the transcript switcher. Returns 404 with code `NOT_FOUND` when the session does not exist.", operationId: "listSessionTranscripts", parameters: [ { $ref: "#/components/parameters/SessionIdPath", example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", }, ], responses: { 200: { description: "List of transcripts available for the session", content: { "application/json": { schema: { $ref: "#/components/schemas/TranscriptListResponse" }, example: { transcripts: [ { id: "main", name: "Main Agent", type: "main", has_transcript: true, db_agent_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main", }, { id: "ad18a79192af10ed1", name: "Explore pricing module", type: "subagent", subagent_type: "Explore", has_transcript: true, db_agent_id: "ad18a79192af10ed1", }, { id: "acompact-7c1e2f90", name: "Context Compaction", type: "compaction", subagent_type: null, has_transcript: true, db_agent_id: null, }, ], }, }, }, }, 404: { description: "Session not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "NOT_FOUND", message: "Session not found" } }, }, }, }, }, }, }, "/api/sessions/{id}/transcript": { get: { tags: ["Sessions"], summary: "Stream messages from a specific transcript", description: "Returns parsed, renderable messages from a JSONL transcript with cursor-based pagination, reading the live file under ~/.claude/projects and falling back to the durable import-time snapshot. Pass `agent_id` to select a specific subagent or compaction transcript (default is the session's main transcript). Pagination cursors are mutually exclusive: `after` returns messages strictly newer than a JSONL line number (incremental live updates on `new_event`), `before` returns messages strictly older than a line (load-on-scroll-up), and `offset` is legacy start-offset paging. `last_line`/`first_line` are the JSONL line numbers of the newest/oldest returned message — feed them back as `after`/`before`. When the session, transcript file, or path cannot be found the endpoint degrades gracefully to an empty result (`messages: []`, `total: 0`, `has_more: false`) rather than erroring. Read-only, no side effects.", operationId: "getSessionTranscript", parameters: [ { $ref: "#/components/parameters/SessionIdPath", example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", }, { name: "agent_id", in: "query", schema: { type: "string" }, description: "Transcript identifier — 'main' for the session's main transcript, or a subagent / compaction id from /transcripts.", example: "main", }, { name: "limit", in: "query", schema: { type: "integer", default: 50, minimum: 1, maximum: 500 }, description: "Maximum number of messages to return.", example: 50, }, { name: "offset", in: "query", schema: { type: "integer", minimum: 0 }, description: "Offset from the start of the transcript (mutually exclusive with after/before).", example: 0, }, { name: "after", in: "query", schema: { type: "integer", minimum: 0 }, description: "Only return messages whose JSONL line number is strictly greater than this value. Used for incremental live updates.", example: 842, }, { name: "before", in: "query", schema: { type: "integer", minimum: 0 }, description: "Only return messages whose JSONL line number is strictly less than this value. Used to load older messages on scroll-up.", example: 200, }, ], responses: { 200: { description: "Parsed messages with cursor metadata", content: { "application/json": { schema: { $ref: "#/components/schemas/TranscriptResponse" }, example: { messages: [ { type: "user", timestamp: "2026-06-25T14:02:11.004Z", content: [ { type: "text", text: "Refactor the pricing route and add a cost endpoint." }, ], }, { type: "assistant", timestamp: "2026-06-25T14:02:18.771Z", model: "claude-opus-4-20250514", content: [ { type: "thinking", text: "I'll start by reading server/routes/pricing.js." }, { type: "tool_use", name: "Read", id: "toolu_01A7c2Df9", input: { file_path: "server/routes/pricing.js" }, }, ], usage: { input_tokens: 412, output_tokens: 96 }, }, { type: "user", timestamp: "2026-06-25T14:02:19.330Z", content: [ { type: "tool_result", id: "toolu_01A7c2Df9", output: 'const { Router } = require("express");\n...', is_error: false, }, ], }, ], total: 1284, has_more: true, last_line: 5310, first_line: 5301, }, }, }, }, 404: { description: "Session or transcript not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "NOT_FOUND", message: "Session not found" } }, }, }, }, }, }, }, "/api/agents": { get: { tags: ["Agents"], summary: "List agents", description: "Returns agents, most recent first. Filters are applied with precedence rather than composition: when `session_id` is supplied it wins and returns every agent for that session (ignoring `status` and pagination); otherwise a `status` filter returns paginated agents in that lifecycle state; otherwise all agents are returned paginated. `limit` defaults to 10000 when not a positive integer. Read-only, no side effects. Each agent's `metadata` is returned as a raw JSON-encoded string, not a parsed object.", operationId: "listAgents", parameters: [ { $ref: "#/components/parameters/AgentStatusQuery", example: "working" }, { $ref: "#/components/parameters/SessionFilterQuery", example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", }, { $ref: "#/components/parameters/SourcesQuery", example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11", }, { $ref: "#/components/parameters/LimitQuery", example: 50 }, { $ref: "#/components/parameters/OffsetQuery", example: 0 }, ], responses: { 200: { description: "Agent list", content: { "application/json": { schema: { $ref: "#/components/schemas/AgentsListResponse" }, example: { agents: [exampleMainAgent, exampleSubagent], limit: 50, offset: 0, }, }, }, }, }, }, post: { tags: ["Agents"], summary: "Create agent (idempotent)", description: 'Creates an agent keyed by `id`. The operation is idempotent: if an agent with that `id` already exists it is returned untouched with `created: false` and HTTP 200; only a brand-new row yields `created: true` and HTTP 201. Omitted optional fields default server-side — `type` to `"main"`, `status` to `"waiting"` — and other unspecified columns are stored as null. `metadata` is accepted as a JSON object but persisted (and returned) as a JSON-encoded string. A successful create broadcasts an `agent_created` websocket frame. Missing `id`, `session_id`, or `name` returns 400 with code `INVALID_INPUT`.', operationId: "createAgent", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/AgentCreateRequest" }, example: { id: "ad18a79192af10ed1", session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d", name: "Explore pricing module", type: "subagent", subagent_type: "Explore", status: "working", task: "Map every caller of calculateCost() across server/routes", parent_agent_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main", metadata: { spawned_by: "Task" }, }, }, }, }, responses: { 201: { description: "Agent created", content: { "application/json": { schema: { $ref: "#/components/schemas/AgentCreateResponse" }, example: { agent: { ...exampleSubagent, status: "working", ended_at: null, metadata: '{"spawned_by":"Task"}', updated_at: "2026-06-25T14:10:22.310Z", }, created: true, }, }, }, }, 200: { description: "Agent already exists", content: { "application/json": { schema: { $ref: "#/components/schemas/AgentCreateResponse" }, example: { agent: exampleSubagent, created: false, }, }, }, }, 400: { description: "Invalid request body", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "INVALID_INPUT", message: "id, session_id, and name are required" }, }, }, }, }, }, }, }, "/api/agents/{id}": { get: { tags: ["Agents"], summary: "Get agent", description: "Returns a single agent by `id`. Read-only, no side effects. The agent's `metadata` is returned as a raw JSON-encoded string, not a parsed object. Returns 404 with code `NOT_FOUND` when no agent matches the path `id`.", operationId: "getAgent", parameters: [{ $ref: "#/components/parameters/AgentIdPath", example: "ad18a79192af10ed1" }], responses: { 200: { description: "Agent details", content: { "application/json": { schema: { $ref: "#/components/schemas/AgentDetailResponse" }, example: { agent: exampleSubagent }, }, }, }, 404: { description: "Agent not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "NOT_FOUND", message: "Agent not found" } }, }, }, }, }, }, patch: { tags: ["Agents"], summary: "Update agent", description: "Partially updates an agent by `id`. Accepts `name`, `status`, `task`, `current_tool`, `ended_at`, and `metadata`. The UPDATE uses COALESCE, so any field omitted (passed as null) leaves the existing column value unchanged — with one deliberate exception: `current_tool` is written through verbatim when present in the body, so it can be explicitly cleared to null (e.g. when a tool call finishes). `metadata` is supplied as a JSON object but stored and returned as a JSON-encoded string. A successful update re-reads the row and broadcasts an `agent_updated` websocket frame. Returns 404 with code `NOT_FOUND` when the agent does not exist.", operationId: "updateAgent", parameters: [{ $ref: "#/components/parameters/AgentIdPath", example: "ad18a79192af10ed1" }], requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/AgentUpdateRequest" }, example: { status: "completed", current_tool: null, ended_at: "2026-06-25T14:14:09.882Z", }, }, }, }, responses: { 200: { description: "Agent updated", content: { "application/json": { schema: { $ref: "#/components/schemas/AgentUpdateResponse" }, example: { agent: { ...exampleSubagent, status: "completed", current_tool: null, ended_at: "2026-06-25T14:14:09.882Z", updated_at: "2026-06-25T14:14:09.882Z", }, }, }, }, }, 404: { description: "Agent not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, example: { error: { code: "NOT_FOUND", message: "Agent not found" } }, }, }, }, }, }, }, }; module.exports = { tags, schemas, paths };