summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/history.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-18 01:21:30 +0100
committerGitHub <noreply@github.com>2026-05-18 01:21:30 +0100
commitf240675da99bc7705e473b8f70a2628812aa4c10 (patch)
tree3ee2d24b431ccb8cd1a3013c86b34a5782a3e224 /makima/src/server/handlers/history.rs
parent0d996cf7590e3e52f424859c7d6f0e68640f119e (diff)
downloadsoryu-master.tar.gz
soryu-master.zip
chore: drop legacy contracts + supervisor task-grouping (#136)HEADmaster
The contracts table, supervisor task type, and all their backing machinery have been inert for several PRs. The directives system reads its own active contract body for spec text, and PR #135 removed the last LLM surface that spawned supervisors. This PR wipes the dead surface in one shot — the user authorised a DB wipe, so the migration drops every legacy table with CASCADE rather than carrying forward stub rows. Net change: −12k LOC across handlers, repository, state, models, the TUI, and the listen module. What's gone: - contracts, contract_chat_*, contract_events, contract_repositories, contract_type_templates tables. - supervisor_states, supervisor_heartbeats tables. - mesh_chat_conversations, mesh_chat_messages tables. - tasks.contract_id/is_supervisor/supervisor_task_id/supervisor_worktree_task_id columns. - directive_steps.contract_id/contract_type columns. - files.contract_id/contract_phase columns. - history_events.contract_id/phase columns. - The Contract/Supervisor/MeshChat handler + model + repository surface, plus the daemon TUI views that read them. - The standalone listen.rs websocket handler (orphaned with the LLM). What stays: - mesh_supervisor handler: trimmed to just the questions + orders backchannel used by `makima directive ask` / `create-order` (kept the URL prefix for CLI client compat). - directive_documents (the user-facing "contracts" surface). - pending_questions in-memory state for the directive Ask flow. cargo check, cargo test --lib (68 passed), tsc, and vite build all clean. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'makima/src/server/handlers/history.rs')
-rw-r--r--makima/src/server/handlers/history.rs268
1 files changed, 10 insertions, 258 deletions
diff --git a/makima/src/server/handlers/history.rs b/makima/src/server/handlers/history.rs
index bee6b02..46be7ac 100644
--- a/makima/src/server/handlers/history.rs
+++ b/makima/src/server/handlers/history.rs
@@ -10,10 +10,7 @@ use uuid::Uuid;
use crate::{
db::{
- models::{
- flexible_datetime, ContractHistoryResponse, ConversationMessage, HistoryQueryFilters,
- SupervisorConversationResponse, TaskConversationResponse, TaskReference,
- },
+ models::{flexible_datetime, ConversationMessage, HistoryQueryFilters, TaskConversationResponse},
repository,
},
server::{auth::Authenticated, messages::ApiError, state::SharedState},
@@ -32,7 +29,6 @@ pub struct TaskConversationParams {
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimelineQueryFilters {
- pub contract_id: Option<Uuid>,
pub task_id: Option<Uuid>,
pub include_subtasks: Option<bool>,
#[serde(default, deserialize_with = "flexible_datetime::deserialize")]
@@ -42,231 +38,6 @@ pub struct TimelineQueryFilters {
pub limit: Option<i32>,
}
-/// GET /api/v1/contracts/{id}/history
-/// Returns contract history timeline with filtering and pagination
-#[utoipa::path(
- get,
- path = "/api/v1/contracts/{id}/history",
- params(
- ("id" = Uuid, Path, description = "Contract ID"),
- ("phase" = Option<String>, Query, description = "Filter by phase"),
- ("event_types" = Option<String>, Query, description = "Filter by event types (comma-separated)"),
- ("from" = Option<String>, Query, description = "Start date filter"),
- ("to" = Option<String>, Query, description = "End date filter"),
- ("limit" = Option<i32>, Query, description = "Limit results"),
- ),
- responses(
- (status = 200, description = "Contract history", body = ContractHistoryResponse),
- (status = 401, description = "Unauthorized", body = ApiError),
- (status = 403, description = "Forbidden", body = ApiError),
- (status = 404, description = "Contract not found", body = ApiError),
- (status = 503, description = "Database not configured", body = ApiError),
- (status = 500, description = "Internal server error", body = ApiError),
- ),
- security(
- ("bearer_auth" = []),
- ("api_key" = [])
- ),
- tag = "History"
-)]
-pub async fn get_contract_history(
- State(state): State<SharedState>,
- Path(contract_id): Path<Uuid>,
- Query(filters): Query<HistoryQueryFilters>,
- Authenticated(auth): Authenticated,
-) -> impl IntoResponse {
- let Some(ref pool) = state.db_pool else {
- return (
- StatusCode::SERVICE_UNAVAILABLE,
- Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
- )
- .into_response();
- };
-
- // Verify contract exists and user has access
- let contract = match repository::get_contract_for_owner(pool, contract_id, auth.owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Contract not found")),
- )
- .into_response();
- }
- Err(e) => {
- tracing::error!("Failed to get contract {}: {}", contract_id, e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- };
-
- // Get history events
- match repository::get_contract_history(pool, contract.id, auth.owner_id, &filters).await {
- Ok((events, total_count)) => {
- Json(ContractHistoryResponse {
- contract_id,
- entries: events,
- total_count,
- cursor: None, // TODO: implement cursor pagination
- })
- .into_response()
- }
- Err(e) => {
- tracing::error!("Failed to get contract history: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// GET /api/v1/contracts/{id}/supervisor/conversation
-/// Returns full supervisor conversation with spawned task references
-#[utoipa::path(
- get,
- path = "/api/v1/contracts/{id}/supervisor/conversation",
- params(
- ("id" = Uuid, Path, description = "Contract ID")
- ),
- responses(
- (status = 200, description = "Supervisor conversation", body = SupervisorConversationResponse),
- (status = 401, description = "Unauthorized", body = ApiError),
- (status = 403, description = "Forbidden", body = ApiError),
- (status = 404, description = "Supervisor not found", body = ApiError),
- (status = 503, description = "Database not configured", body = ApiError),
- (status = 500, description = "Internal server error", body = ApiError),
- ),
- security(
- ("bearer_auth" = []),
- ("api_key" = [])
- ),
- tag = "History"
-)]
-pub async fn get_supervisor_conversation(
- State(state): State<SharedState>,
- Path(contract_id): Path<Uuid>,
- Authenticated(auth): Authenticated,
-) -> impl IntoResponse {
- let Some(ref pool) = state.db_pool else {
- return (
- StatusCode::SERVICE_UNAVAILABLE,
- Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
- )
- .into_response();
- };
-
- // Get contract for phase info and ownership check
- let contract = match repository::get_contract_for_owner(pool, contract_id, auth.owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Contract not found")),
- )
- .into_response();
- }
- Err(e) => {
- tracing::error!("Failed to get contract {}: {}", contract_id, e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- };
-
- // Get the supervisor state
- let supervisor_state = match repository::get_supervisor_state(pool, contract_id).await {
- Ok(Some(s)) => s,
- Ok(None) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Supervisor not found")),
- )
- .into_response();
- }
- Err(e) => {
- tracing::error!("Failed to get supervisor state for {}: {}", contract_id, e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- };
-
- // Parse conversation history from JSONB
- let messages: Vec<ConversationMessage> = supervisor_state
- .conversation_history
- .as_array()
- .map(|arr| {
- arr.iter()
- .enumerate()
- .map(|(i, v)| ConversationMessage {
- id: i.to_string(),
- role: v
- .get("role")
- .and_then(|r| r.as_str())
- .unwrap_or("user")
- .to_string(),
- content: v
- .get("content")
- .and_then(|c| c.as_str())
- .unwrap_or("")
- .to_string(),
- timestamp: supervisor_state.last_activity,
- tool_calls: None,
- tool_name: None,
- tool_input: None,
- tool_result: None,
- is_error: None,
- token_count: None,
- cost_usd: None,
- })
- .collect()
- })
- .unwrap_or_default();
-
- // Get spawned tasks
- let tasks = match repository::list_tasks_by_contract(pool, contract_id, auth.owner_id).await {
- Ok(t) => t,
- Err(e) => {
- tracing::warn!("Failed to get tasks for contract {}: {}", contract_id, e);
- Vec::new()
- }
- };
-
- let spawned_tasks: Vec<TaskReference> = tasks
- .into_iter()
- .filter(|t| !t.is_supervisor)
- .map(|t| TaskReference {
- task_id: t.id,
- task_name: t.name,
- status: t.status,
- created_at: t.created_at,
- completed_at: t.completed_at,
- })
- .collect();
-
- Json(SupervisorConversationResponse {
- contract_id,
- supervisor_task_id: supervisor_state.task_id,
- phase: contract.phase,
- last_activity: supervisor_state.last_activity,
- pending_task_ids: supervisor_state.pending_task_ids,
- messages,
- spawned_tasks,
- })
- .into_response()
-}
-
-/// GET /api/v1/mesh/tasks/{id}/conversation
-/// Returns task conversation history
#[utoipa::path(
get,
path = "/api/v1/mesh/tasks/{id}/conversation",
@@ -364,28 +135,16 @@ pub async fn get_task_conversation(
}
/// GET /api/v1/timeline
-/// Returns unified timeline for authenticated user
+/// Returns unified task-history timeline for the authenticated user.
#[utoipa::path(
get,
path = "/api/v1/timeline",
- params(
- ("contract_id" = Option<Uuid>, Query, description = "Filter by contract"),
- ("task_id" = Option<Uuid>, Query, description = "Filter by task"),
- ("include_subtasks" = Option<bool>, Query, description = "Include subtask events"),
- ("from" = Option<String>, Query, description = "Start date filter"),
- ("to" = Option<String>, Query, description = "End date filter"),
- ("limit" = Option<i32>, Query, description = "Limit results"),
- ),
responses(
- (status = 200, description = "Timeline events", body = ContractHistoryResponse),
+ (status = 200, description = "Timeline events"),
(status = 401, description = "Unauthorized", body = ApiError),
(status = 503, description = "Database not configured", body = ApiError),
- (status = 500, description = "Internal server error", body = ApiError),
- ),
- security(
- ("bearer_auth" = []),
- ("api_key" = [])
),
+ security(("bearer_auth" = []), ("api_key" = [])),
tag = "History"
)]
pub async fn get_timeline(
@@ -402,7 +161,6 @@ pub async fn get_timeline(
};
let history_filters = HistoryQueryFilters {
- phase: None,
event_types: None,
from: filters.from,
to: filters.to,
@@ -410,24 +168,18 @@ pub async fn get_timeline(
cursor: None,
};
- let result = if let Some(contract_id) = filters.contract_id {
- repository::get_contract_history(pool, contract_id, auth.owner_id, &history_filters).await
- } else if let Some(task_id) = filters.task_id {
+ let result = if let Some(task_id) = filters.task_id {
repository::get_task_history(pool, task_id, auth.owner_id, &history_filters).await
} else {
repository::get_timeline(pool, auth.owner_id, &history_filters).await
};
match result {
- Ok((events, total_count)) => {
- Json(ContractHistoryResponse {
- contract_id: filters.contract_id.unwrap_or_default(),
- entries: events,
- total_count,
- cursor: None,
- })
- .into_response()
- }
+ Ok((events, total_count)) => Json(serde_json::json!({
+ "entries": events,
+ "totalCount": total_count,
+ }))
+ .into_response(),
Err(e) => {
tracing::error!("Failed to get timeline: {}", e);
(