From 92dc29eb6ffdd9131e44a80e0b49b4be11e7f33b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 6 Mar 2026 09:51:50 -0500 Subject: [PATCH] Sessions API review fixes: add updated_at to create response, UUID session IDs, remove misleading SSE schemas - Add updated_at to CreateSessionResponse for consistency with SessionListItem/SessionDetail - Add format: uuid to session ID fields and parameter across the OpenAPI spec - Remove SessionEvent discriminated union and SessionEvent* wrapper schemas that conflated SSE transport-level event names with JSON data payload fields - Update demo data to use proper UUIDs instead of string IDs - Add uuid dependency to arc-types crate Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 2 + crates/arc-api/src/demo/mod.rs | 49 +++++++---- crates/arc-types/Cargo.toml | 1 + docs/api-reference/arc-api.yaml | 87 ++++--------------- .../src/.openapi-generator/FILES | 5 -- .../arc-api-client/src/api/sessions-api.ts | 8 +- .../src/models/create-session-response.ts | 4 + packages/arc-api-client/src/models/index.ts | 5 -- 8 files changed, 60 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06a4843ed..e0fad3e00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,6 +317,7 @@ dependencies = [ "serde_yaml", "syn", "typify", + "uuid", ] [[package]] @@ -5069,6 +5070,7 @@ checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "getrandom 0.4.1", "js-sys", + "serde_core", "wasm-bindgen", ] diff --git a/crates/arc-api/src/demo/mod.rs b/crates/arc-api/src/demo/mod.rs index 1330e87a9..b06dcad51 100644 --- a/crates/arc-api/src/demo/mod.rs +++ b/crates/arc-api/src/demo/mod.rs @@ -501,9 +501,11 @@ pub async fn create_session_stub( _auth: AuthenticatedService, State(_state): State>, ) -> Response { + let id = uuid::Uuid::new_v4(); + let now = "2026-03-06T16:00:00Z"; ( StatusCode::CREATED, - Json(serde_json::json!({"id": "demo-session-new", "title": "New session", "model": "Opus 4.6", "created_at": "2026-03-06T16:00:00Z"})), + Json(serde_json::json!({"id": id, "title": "New session", "model": "Opus 4.6", "created_at": now, "updated_at": now})), ) .into_response() } @@ -2367,15 +2369,29 @@ mod retros { mod sessions { use arc_types::*; use chrono::{DateTime, Utc}; + use uuid::Uuid; fn ts(s: &str) -> DateTime { s.parse().unwrap() } + fn uid(n: u128) -> Uuid { + Uuid::from_u128(n) + } + + const S1: u128 = 0x10000000_0000_4000_8000_000000000001; + const S2: u128 = 0x10000000_0000_4000_8000_000000000002; + const S3: u128 = 0x10000000_0000_4000_8000_000000000003; + const S4: u128 = 0x10000000_0000_4000_8000_000000000004; + const S5: u128 = 0x10000000_0000_4000_8000_000000000005; + const S6: u128 = 0x10000000_0000_4000_8000_000000000006; + const S7: u128 = 0x10000000_0000_4000_8000_000000000007; + const S8: u128 = 0x10000000_0000_4000_8000_000000000008; + pub fn list_items() -> Vec { vec![ SessionListItem { - id: "s1".into(), + id: uid(S1), title: "Add rate limiting to auth endpoints".into(), model: "Opus 4.6".into(), last_message_preview: "Done. I've created the rate limiter and wired it up...".into(), @@ -2383,7 +2399,7 @@ mod sessions { updated_at: ts("2026-03-06T15:45:00Z"), }, SessionListItem { - id: "s2".into(), + id: uid(S2), title: "Fix config parsing for nested values".into(), model: "Sonnet 4.6".into(), last_message_preview: "Fixed. The parser now tracks the current section header...".into(), @@ -2391,7 +2407,7 @@ mod sessions { updated_at: ts("2026-03-06T13:15:00Z"), }, SessionListItem { - id: "s3".into(), + id: uid(S3), title: "Migrate to React Router v7".into(), model: "Opus 4.6".into(), last_message_preview: "You're on React Router 6.22. The migration to v7 involves...".into(), @@ -2399,7 +2415,7 @@ mod sessions { updated_at: ts("2026-03-05T11:30:00Z"), }, SessionListItem { - id: "s4".into(), + id: uid(S4), title: "Add dark mode toggle".into(), model: "Sonnet 4.6".into(), last_message_preview: "Added a dark mode toggle to the settings panel with system preference detection.".into(), @@ -2407,7 +2423,7 @@ mod sessions { updated_at: ts("2026-03-05T09:45:00Z"), }, SessionListItem { - id: "s5".into(), + id: uid(S5), title: "Update OpenAPI spec for v3".into(), model: "Opus 4.6".into(), last_message_preview: "Updated all endpoint schemas to v3 format with discriminated unions.".into(), @@ -2415,7 +2431,7 @@ mod sessions { updated_at: ts("2026-03-05T08:30:00Z"), }, SessionListItem { - id: "s6".into(), + id: uid(S6), title: "Terraform module for Redis cluster".into(), model: "Opus 4.6".into(), last_message_preview: "Created the module with 3-node cluster, automatic failover, and encryption at rest.".into(), @@ -2423,7 +2439,7 @@ mod sessions { updated_at: ts("2026-03-03T16:00:00Z"), }, SessionListItem { - id: "s7".into(), + id: uid(S7), title: "Add pipeline event types".into(), model: "Sonnet 4.6".into(), last_message_preview: "Added PipelineStarted, StageCompleted, and PipelineFailed event types.".into(), @@ -2431,7 +2447,7 @@ mod sessions { updated_at: ts("2026-03-01T12:00:00Z"), }, SessionListItem { - id: "s8".into(), + id: uid(S8), title: "Implement webhook retry logic".into(), model: "Opus 4.6".into(), last_message_preview: "Implemented exponential backoff with jitter, max 5 retries over 24 hours.".into(), @@ -2442,9 +2458,10 @@ mod sessions { } pub fn detail(id: &str) -> Option { - match id { - "s1" => Some(SessionDetail { - id: "s1".into(), title: "Add rate limiting to auth endpoints".into(), model: "Opus 4.6".into(), created_at: ts("2026-03-06T14:30:00Z"), updated_at: ts("2026-03-06T15:45:00Z"), + let parsed = id.parse::().ok()?; + match parsed.as_u128() { + S1 => Some(SessionDetail { + id: uid(S1), title: "Add rate limiting to auth endpoints".into(), model: "Opus 4.6".into(), created_at: ts("2026-03-06T14:30:00Z"), updated_at: ts("2026-03-06T15:45:00Z"), turns: vec![ SessionTurn::UserTurn(UserTurn { kind: UserTurnKind::User, content: "Add rate limiting to the auth endpoints. We're getting hit with brute force attempts on /api/auth/login and /api/auth/register. Use a sliding window approach with Redis, 10 requests per minute per IP.".into(), created_at: ts("2026-02-28T10:00:00Z") }), SessionTurn::AssistantTurn(AssistantTurn { kind: AssistantTurnKind::Assistant, content: "I'll implement sliding window rate limiting using Redis. Let me first look at the existing auth routes and middleware setup.".into(), created_at: ts("2026-02-28T10:00:15Z") }), @@ -2462,8 +2479,8 @@ mod sessions { SessionTurn::AssistantTurn(AssistantTurn { kind: AssistantTurnKind::Assistant, content: "Done. I've created the rate limiter and wired it up:\n\n- **`src/middleware/rate-limit.ts`** \u{2014} Sliding window rate limiter using Redis sorted sets. Each request adds a timestamped entry, expired entries are pruned, and the count is checked against the limit.\n- **`src/routes/auth.ts`** \u{2014} Applied `rateLimit({ windowMs: 60_000, max: 10 })` to both `/login` and `/register`.\n- **`src/middleware/index.ts`** \u{2014} Re-exported the new middleware.\n\nWhen the limit is exceeded, clients get a `429` response with a `Retry-After` header indicating seconds until the window resets.".into(), created_at: ts("2026-02-28T10:01:00Z") }), ], }), - "s2" => Some(SessionDetail { - id: "s2".into(), title: "Fix config parsing for nested values".into(), model: "Sonnet 4.6".into(), created_at: ts("2026-03-06T12:30:00Z"), updated_at: ts("2026-03-06T13:15:00Z"), + S2 => Some(SessionDetail { + id: uid(S2), title: "Fix config parsing for nested values".into(), model: "Sonnet 4.6".into(), created_at: ts("2026-03-06T12:30:00Z"), updated_at: ts("2026-03-06T13:15:00Z"), turns: vec![ SessionTurn::UserTurn(UserTurn { kind: UserTurnKind::User, content: "The CLI crashes when parsing nested TOML config values like [database.connection]. Can you debug and fix this?".into(), created_at: ts("2026-02-28T10:00:00Z") }), SessionTurn::AssistantTurn(AssistantTurn { kind: AssistantTurnKind::Assistant, content: "Let me look at the config parser to understand how nested keys are handled.".into(), created_at: ts("2026-02-28T10:00:12Z") }), @@ -2478,8 +2495,8 @@ mod sessions { SessionTurn::AssistantTurn(AssistantTurn { kind: AssistantTurnKind::Assistant, content: "Fixed. The parser now:\n\n1. Tracks the current section header (e.g. `[database.connection]`)\n2. Constructs nested objects from dotted paths\n3. Handles both `key = value` within sections and bare `dotted.key = value` syntax\n\nAll 4 tests pass, including the new nested section case.".into(), created_at: ts("2026-02-28T10:01:00Z") }), ], }), - "s3" => Some(SessionDetail { - id: "s3".into(), title: "Migrate to React Router v7".into(), model: "Opus 4.6".into(), created_at: ts("2026-03-05T10:00:00Z"), updated_at: ts("2026-03-05T11:30:00Z"), + S3 => Some(SessionDetail { + id: uid(S3), title: "Migrate to React Router v7".into(), model: "Opus 4.6".into(), created_at: ts("2026-03-05T10:00:00Z"), updated_at: ts("2026-03-05T11:30:00Z"), turns: vec![ SessionTurn::UserTurn(UserTurn { kind: UserTurnKind::User, content: "Help me migrate our app from React Router v6 to v7. We're using createBrowserRouter with data loaders.".into(), created_at: ts("2026-02-26T10:00:00Z") }), SessionTurn::AssistantTurn(AssistantTurn { kind: AssistantTurnKind::Assistant, content: "I'll audit your current router setup and identify what needs to change for v7. Let me scan the codebase.".into(), created_at: ts("2026-02-26T10:00:10Z") }), diff --git a/crates/arc-types/Cargo.toml b/crates/arc-types/Cargo.toml index da4aa03cb..afd35aa2f 100644 --- a/crates/arc-types/Cargo.toml +++ b/crates/arc-types/Cargo.toml @@ -12,6 +12,7 @@ doctest = false chrono = { workspace = true, features = ["serde"] } serde.workspace = true serde_json.workspace = true +uuid = { workspace = true, features = ["serde"] } [build-dependencies] typify = { version = "0.6", default-features = false } diff --git a/docs/api-reference/arc-api.yaml b/docs/api-reference/arc-api.yaml index 12dd33be0..f3e705ccc 100644 --- a/docs/api-reference/arc-api.yaml +++ b/docs/api-reference/arc-api.yaml @@ -814,14 +814,14 @@ paths: description: | Opens a server-sent event (SSE) stream for real-time session updates. - The stream emits the following event types: + The stream emits the following SSE event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{"message": "..."}` (error occurred) - See the `SessionEvent` schema for the discriminated union of all event payloads. + Each SSE frame has an `event:` line (the event type) and a `data:` line (the JSON payload). parameters: - $ref: "#/components/parameters/SessionId" responses: @@ -1072,7 +1072,8 @@ components: description: Unique session identifier. schema: type: string - example: s1 + format: uuid + example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 PageLimit: name: page[limit] @@ -2091,8 +2092,9 @@ components: properties: id: type: string + format: uuid description: Unique session identifier. - example: s1 + example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 title: type: string description: Short title summarizing the session topic. @@ -2207,8 +2209,9 @@ components: properties: id: type: string + format: uuid description: Unique session identifier. - example: s1 + example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 title: type: string description: Short title summarizing the session topic. @@ -2256,11 +2259,13 @@ components: - title - model - created_at + - updated_at properties: id: type: string + format: uuid description: Unique identifier for the newly created session. - example: s42 + example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 title: type: string description: Server-generated title for the session. @@ -2274,6 +2279,11 @@ components: format: date-time description: Timestamp when the session was created. example: "2026-03-06T16:00:00Z" + updated_at: + type: string + format: date-time + description: Timestamp when the session was last updated (equal to created_at at creation time). + example: "2026-03-06T16:00:00Z" SendMessageRequest: description: Request body for sending a follow-up message in an existing session. @@ -2297,71 +2307,6 @@ components: description: Whether the message was accepted for processing. example: true - SessionEvent: - description: Discriminated union of all SSE event payloads. - discriminator: - propertyName: event - mapping: - assistant_turn: "#/components/schemas/SessionEventAssistantTurn" - tool_turn: "#/components/schemas/SessionEventToolTurn" - done: "#/components/schemas/SessionEventDone" - error: "#/components/schemas/SessionEventError" - oneOf: - - $ref: "#/components/schemas/SessionEventAssistantTurn" - - $ref: "#/components/schemas/SessionEventToolTurn" - - $ref: "#/components/schemas/SessionEventDone" - - $ref: "#/components/schemas/SessionEventError" - - SessionEventAssistantTurn: - description: An assistant turn event. - type: object - required: - - event - - data - properties: - event: - type: string - enum: [assistant_turn] - data: - $ref: "#/components/schemas/AssistantTurn" - - SessionEventToolTurn: - description: A tool turn event. - type: object - required: - - event - - data - properties: - event: - type: string - enum: [tool_turn] - data: - $ref: "#/components/schemas/ToolTurn" - - SessionEventDone: - description: Stream completion event. - type: object - required: - - event - properties: - event: - type: string - enum: [done] - - SessionEventError: - description: Error event. - type: object - required: - - event - - message - properties: - event: - type: string - enum: [error] - message: - type: string - description: Human-readable error message. - # ── Insights Schemas ──────────────────────────────────────────────── SavedQuery: diff --git a/packages/arc-api-client/src/.openapi-generator/FILES b/packages/arc-api-client/src/.openapi-generator/FILES index 438fd4a22..bbd7cd112 100644 --- a/packages/arc-api-client/src/.openapi-generator/FILES +++ b/packages/arc-api-client/src/.openapi-generator/FILES @@ -78,11 +78,6 @@ models/saved-query.ts models/send-message-request.ts models/send-message-response.ts models/session-detail.ts -models/session-event-assistant-turn.ts -models/session-event-done.ts -models/session-event-error.ts -models/session-event-tool-turn.ts -models/session-event.ts models/session-list-item.ts models/session-turn.ts models/setting-field-type.ts diff --git a/packages/arc-api-client/src/api/sessions-api.ts b/packages/arc-api-client/src/api/sessions-api.ts index eb5ee8d19..a56e309eb 100644 --- a/packages/arc-api-client/src/api/sessions-api.ts +++ b/packages/arc-api-client/src/api/sessions-api.ts @@ -217,7 +217,7 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat }; }, /** - * Opens a server-sent event (SSE) stream for real-time session updates. The stream emits the following event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{\"message\": \"...\"}` (error occurred) See the `SessionEvent` schema for the discriminated union of all event payloads. + * Opens a server-sent event (SSE) stream for real-time session updates. The stream emits the following SSE event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{\"message\": \"...\"}` (error occurred) Each SSE frame has an `event:` line (the event type) and a `data:` line (the JSON payload). * @summary Stream Session Events * @param {string} id Unique session identifier. * @param {*} [options] Override http request option. @@ -321,7 +321,7 @@ export const SessionsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Opens a server-sent event (SSE) stream for real-time session updates. The stream emits the following event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{\"message\": \"...\"}` (error occurred) See the `SessionEvent` schema for the discriminated union of all event payloads. + * Opens a server-sent event (SSE) stream for real-time session updates. The stream emits the following SSE event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{\"message\": \"...\"}` (error occurred) Each SSE frame has an `event:` line (the event type) and a `data:` line (the JSON payload). * @summary Stream Session Events * @param {string} id Unique session identifier. * @param {*} [options] Override http request option. @@ -385,7 +385,7 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP return localVarFp.sendSessionMessage(id, sendMessageRequest, options).then((request) => request(axios, basePath)); }, /** - * Opens a server-sent event (SSE) stream for real-time session updates. The stream emits the following event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{\"message\": \"...\"}` (error occurred) See the `SessionEvent` schema for the discriminated union of all event payloads. + * Opens a server-sent event (SSE) stream for real-time session updates. The stream emits the following SSE event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{\"message\": \"...\"}` (error occurred) Each SSE frame has an `event:` line (the event type) and a `data:` line (the JSON payload). * @summary Stream Session Events * @param {string} id Unique session identifier. * @param {*} [options] Override http request option. @@ -448,7 +448,7 @@ export class SessionsApi extends BaseAPI { } /** - * Opens a server-sent event (SSE) stream for real-time session updates. The stream emits the following event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{\"message\": \"...\"}` (error occurred) See the `SessionEvent` schema for the discriminated union of all event payloads. + * Opens a server-sent event (SSE) stream for real-time session updates. The stream emits the following SSE event types: - `event: assistant_turn` — data: `AssistantTurn` JSON object - `event: tool_turn` — data: `ToolTurn` JSON object - `event: done` — data: `{}` (stream complete) - `event: error` — data: `{\"message\": \"...\"}` (error occurred) Each SSE frame has an `event:` line (the event type) and a `data:` line (the JSON payload). * @summary Stream Session Events * @param {string} id Unique session identifier. * @param {*} [options] Override http request option. diff --git a/packages/arc-api-client/src/models/create-session-response.ts b/packages/arc-api-client/src/models/create-session-response.ts index 602f77c15..aa26b3c4e 100644 --- a/packages/arc-api-client/src/models/create-session-response.ts +++ b/packages/arc-api-client/src/models/create-session-response.ts @@ -34,5 +34,9 @@ export interface CreateSessionResponse { * Timestamp when the session was created. */ 'created_at': string; + /** + * Timestamp when the session was last updated (equal to created_at at creation time). + */ + 'updated_at': string; } diff --git a/packages/arc-api-client/src/models/index.ts b/packages/arc-api-client/src/models/index.ts index 39b8e8e41..aac00499e 100644 --- a/packages/arc-api-client/src/models/index.ts +++ b/packages/arc-api-client/src/models/index.ts @@ -59,11 +59,6 @@ export * from './saved-query'; export * from './send-message-request'; export * from './send-message-response'; export * from './session-detail'; -export * from './session-event'; -export * from './session-event-assistant-turn'; -export * from './session-event-done'; -export * from './session-event-error'; -export * from './session-event-tool-turn'; export * from './session-list-item'; export * from './session-turn'; export * from './setting-field';