Files
kandev/docs/task-session-turns.md
T

167 lines
7.8 KiB
Markdown

# Task Session Turns: Table + Flow
This document describes how to add a turn table that groups all messages from a single agent turn, how messages link to a turn, and how turn updates propagate through the backend → event bus → WebSocket → frontend store.
## Goals
- Add a `task_session_turns` table with at least `id`, `started_at`, `completed_at`.
- Ensure every `task_session_message` points to the turn it belongs to.
- Turn creation/completion follows existing app flow and broadcasts to subscribed clients.
## Definitions
- **Turn**: A single prompt/response cycle for a task session, starting when a prompt is sent and ending when the agent completes/cancels/errors.
- **Message**: A persisted item in `task_session_messages` (user prompt, agent text, tool call, status, error).
## Schema Changes (SQLite)
Add a new table and link messages to it.
### New Table: `task_session_turns`
Suggested columns (minimum + useful metadata):
- `id` TEXT PRIMARY KEY (UUID).
- `task_session_id` TEXT NOT NULL (FK → `task_sessions.id`).
- `task_id` TEXT NOT NULL (denormalized for query fanout).
- `started_at` DATETIME NOT NULL.
- `completed_at` DATETIME (nullable).
- `created_at` DATETIME NOT NULL.
- `updated_at` DATETIME NOT NULL.
- `metadata` TEXT DEFAULT '{}' (optional, for provider-specific data like Codex `turn_id`).
Indexes:
- `idx_turns_session_id` on `(task_session_id)`.
- `idx_turns_session_started` on `(task_session_id, started_at)`.
- `idx_turns_task_id` on `(task_id)`.
- Optional: partial index for active turn lookup `(task_session_id) WHERE completed_at IS NULL`.
### Link Messages to Turns
Add a `turn_id` column to `task_session_messages`:
- `turn_id` TEXT (FK → `task_session_turns.id`).
- Index on `(turn_id)`.
- Keep nullable for legacy messages; new writes should set it.
Files to update:
- `apps/backend/internal/task/repository/sqlite.go` (schema + migration via `ensureColumn`).
- `apps/backend/internal/task/models/models.go` (add `Turn` model, add `TurnID` to `Message`).
- `apps/backend/pkg/api/v1/task.go` and `apps/web/lib/types/http.ts` (API types include `turn_id`).
## Backend Flow (Turn Lifecycle)
Turns should align with the prompt lifecycle already handled in the orchestrator and message services.
### Create Turn
Create a turn when a prompt begins:
- **Entry point**: `apps/backend/internal/orchestrator/service.go` in `PromptTask` before calling `executor.Prompt`.
- Create `task_session_turns` row with `started_at = now`.
- Persist the new `turn_id` in memory for the rest of the prompt lifecycle.
- Suggested: update `task_sessions.metadata.active_turn_id` and/or hold in the `Executor`/`AgentExecution` context.
### Attach Messages to Turn
Ensure all messages created while a prompt is in-flight use the active `turn_id`.
Where messages are created today:
- User prompt: `apps/backend/internal/task/handlers/message_handlers.go` → `MessageController.CreateMessage`.
- Agent output + tool events: `apps/backend/internal/orchestrator/service.go` (`saveAgentTextIfPresent`, `handleToolCallEvent`, `handleToolUpdateEvent`, error/status creation).
Expected behavior:
- The user prompt message should reference the newly created `turn_id`.
- Subsequent agent/tool/status messages should use the same `turn_id`.
- If the session is resumed and no active turn exists, create a new turn before message creation.
Implementation sketch:
- Add `TurnID` to `CreateMessageRequest` (service/controller layers).
- Teach `messageCreator.CreateAgentMessage/CreateToolCallMessage/CreateSessionMessage` to accept a `turn_id` or resolve the current active turn by `session_id`.
- When the orchestrator receives agent stream events, it should pass `turn_id` to message creation calls.
### Complete Turn
Close the turn when the agent finishes or the turn is cancelled:
- **Completion**: `apps/backend/internal/orchestrator/service.go` in `handleAgentStreamEvent` when `eventType == "complete"`.
- **Cancellation**: `apps/backend/internal/orchestrator/service.go` in `CancelAgent`.
- **Errors**: Treat `eventType == "error"` as terminal if it ends the prompt; set `completed_at`.
Action:
- Update `task_session_turns.completed_at = now`, `updated_at = now`.
- Clear `task_sessions.metadata.active_turn_id` (if used).
## Event Bus and WebSocket Broadcast
Turn updates should follow the same path as existing message/session events.
### Event Types
Add new event types in `apps/backend/internal/events/types.go`:
- `TurnStarted = "turn.started"` (or `task_session.turn.started`).
- `TurnCompleted = "turn.completed"`.
- Optional `TurnUpdated = "turn.updated"` for partial updates.
### Publish Events
Publish turn events when you create/complete a turn:
- Location: task service or orchestrator service (same layer that writes to DB).
- Payload fields (minimum):
- `turn_id`
- `session_id`
- `task_id`
- `started_at`
- `completed_at`
### Broadcast to WebSocket Subscribers
Wire up in `apps/backend/internal/gateway/websocket/task_notifications.go`:
- Subscribe to the new event types.
- Broadcast to the session channel (same as `session.message.added`).
Add action constants in `apps/backend/pkg/websocket/actions.go`:
- `ActionSessionTurnStarted = "session.turn.started"`
- `ActionSessionTurnCompleted = "session.turn.completed"`
- Optional `ActionSessionTurnUpdated = "session.turn.updated"`
## Frontend Flow
Frontend store and handlers should mirror the current message flow.
### WebSocket Handlers
Add a new WS handler file (or extend existing):
- `apps/web/lib/ws/handlers/turns.ts` to handle:
- `session.turn.started`
- `session.turn.completed`
- Store updates should be keyed by `session_id`.
### Store Shape
Add a `turns` slice in `apps/web/lib/state/store.ts`:
- `turns.bySession[sessionId] = Turn[]`
- `turns.activeBySession[sessionId] = Turn | null` (optional for UI)
### Message Rendering
Messages already render from `messages.bySession[sessionId]`. With `turn_id`:
- Allow grouping in UI by turn.
- Allow filtering by active turn.
- Show turn duration in the UI (e.g., "running for 42s") by comparing `started_at` to now for active turns.
### SSR Data
If needed for initial render:
- Add an HTTP endpoint to list turns for a session.
- SSR fetch in `apps/web/app/task/[id]/page-client.tsx` or equivalent (see `docs/session-page-data-flow.md`).
## Migration / Backfill
Existing messages will not have `turn_id`:
- Keep `turn_id` nullable.
- Optional backfill: create one turn per session using min/max message timestamps and assign messages with NULL `turn_id` to that turn.
## Edge Cases
- **Multiple prompts queued**: Ensure only one active turn per session. If prompts can overlap, add a `state` column or queue ID.
- **Agent error before messages**: Start turn on prompt, complete on error even if no agent messages.
- **Permission requests**: If the agent pauses for permission, the turn remains open; completion happens after prompt completion or cancellation.
- **Concurrency**: Multiple sessions can run in parallel without conflict as long as turns and broadcasts stay scoped by `session_id` (and `active_turn_id` is per session).
## Touchpoints Summary
Backend:
- `apps/backend/internal/task/models/models.go` (Turn model, Message.TurnID)
- `apps/backend/internal/task/repository/sqlite.go` (schema + queries)
- `apps/backend/internal/task/repository/interface.go` (turn methods)
- `apps/backend/internal/task/service/service.go` (turn create/complete, message association)
- `apps/backend/internal/orchestrator/service.go` (prompt start/complete)
- `apps/backend/internal/events/types.go` (turn events)
- `apps/backend/internal/gateway/websocket/task_notifications.go` (broadcast)
- `apps/backend/pkg/websocket/actions.go` (actions)
Frontend:
- `apps/web/lib/ws/handlers/turns.ts` (WS handlers)
- `apps/web/lib/state/store.ts` (turns slice)
- `apps/web/lib/types/http.ts` (Turn + Message.turn_id)
- `apps/web/components/task/chat/*` (optional turn grouping UI)
Docs:
- `docs/WEBSOCKET_API.md` (document new turn actions and payloads).