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 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-06 09:51:50 -05:00
parent 3a2e91a123
commit 92dc29eb6f
8 changed files with 60 additions and 101 deletions

2
Cargo.lock generated
View file

@ -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",
]

View file

@ -501,9 +501,11 @@ pub async fn create_session_stub(
_auth: AuthenticatedService,
State(_state): State<Arc<AppState>>,
) -> 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<Utc> {
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<SessionListItem> {
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<SessionDetail> {
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::<Uuid>().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") }),

View file

@ -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 }

View file

@ -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:

View file

@ -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

View file

@ -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.

View file

@ -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;
}

View file

@ -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';