diff options
| author | soryu <soryu@soryu.co> | 2026-02-16 00:28:16 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-16 00:28:16 +0000 |
| commit | a9da99085bc0b1f94e13cb27639915fd1398ccbe (patch) | |
| tree | 7b990499368002af8aa72b8e7b619674d8d5c654 | |
| parent | bf087f48af2962d884b861345ae52be4f4a54daa (diff) | |
| download | soryu-a9da99085bc0b1f94e13cb27639915fd1398ccbe.tar.gz soryu-a9da99085bc0b1f94e13cb27639915fd1398ccbe.zip | |
feat: track directive goal history for intelligent re-planning (#63)
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Save previous goal on update and include history in re-planning prompt
| -rw-r--r-- | makima/migrations/20260215100000_add_directive_goal_history.sql | 9 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 10 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 40 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 46 | ||||
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 45 |
5 files changed, 123 insertions, 27 deletions
diff --git a/makima/migrations/20260215100000_add_directive_goal_history.sql b/makima/migrations/20260215100000_add_directive_goal_history.sql new file mode 100644 index 0000000..1636110 --- /dev/null +++ b/makima/migrations/20260215100000_add_directive_goal_history.sql @@ -0,0 +1,9 @@ +CREATE TABLE directive_goal_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE, + goal TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_directive_goal_history_directive_id ON directive_goal_history(directive_id); +CREATE INDEX idx_directive_goal_history_created_at ON directive_goal_history(directive_id, created_at DESC); diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 6ec6cf4..19ebb13 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2723,6 +2723,16 @@ pub struct Directive { pub updated_at: DateTime<Utc>, } +/// A historical record of a directive goal change. +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveGoalHistory { + pub id: Uuid, + pub directive_id: Uuid, + pub goal: String, + pub created_at: DateTime<Utc>, +} + /// A step in a directive's DAG. #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 71ad524..4aa09dc 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -12,8 +12,8 @@ use super::models::{ CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, Directive, DirectiveStep, DirectiveSummary, - CreateDirectiveRequest, CreateDirectiveStepRequest, UpdateDirectiveRequest, - UpdateDirectiveStepRequest, + CreateDirectiveRequest, CreateDirectiveStepRequest, DirectiveGoalHistory, + UpdateDirectiveRequest, UpdateDirectiveStepRequest, CreateOrderRequest, Order, UpdateOrderRequest, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, @@ -5600,6 +5600,42 @@ pub async fn update_directive_goal( .await } +/// Save a goal to the directive goal history. +pub async fn save_directive_goal_history( + pool: &PgPool, + directive_id: Uuid, + goal: &str, +) -> Result<(), sqlx::Error> { + sqlx::query( + r#"INSERT INTO directive_goal_history (directive_id, goal) + VALUES ($1, $2)"#, + ) + .bind(directive_id) + .bind(goal) + .execute(pool) + .await?; + Ok(()) +} + +/// Get recent goal history for a directive (most recent first), limited to limit entries. +pub async fn get_directive_goal_history( + pool: &PgPool, + directive_id: Uuid, + limit: i64, +) -> Result<Vec<DirectiveGoalHistory>, sqlx::Error> { + sqlx::query_as::<_, DirectiveGoalHistory>( + r#"SELECT id, directive_id, goal, created_at + FROM directive_goal_history + WHERE directive_id = $1 + ORDER BY created_at DESC + LIMIT $2"#, + ) + .bind(directive_id) + .bind(limit) + .fetch_all(pool) + .await +} + /// Set a directive's status (used for start/pause/archive transitions). pub async fn set_directive_status( pool: &PgPool, diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 5f8cb486..92aacde 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -44,7 +44,7 @@ impl DirectiveOrchestrator { "Directive needs planning — spawning planning task" ); - let plan = build_planning_prompt(&directive, &[], 1); + let plan = build_planning_prompt(&directive, &[], 1, &[]); if let Err(e) = self .spawn_orchestrator_task( @@ -317,8 +317,11 @@ impl DirectiveOrchestrator { repository::list_directive_steps(&self.pool, directive.id).await?; let generation = repository::get_directive_max_generation(&self.pool, directive.id).await? + 1; + let goal_history = + repository::get_directive_goal_history(&self.pool, directive.id, 3).await?; - let plan = build_planning_prompt(&directive, &existing_steps, generation); + let plan = + build_planning_prompt(&directive, &existing_steps, generation, &goal_history); if let Err(e) = self .spawn_orchestrator_task( @@ -846,6 +849,7 @@ fn build_planning_prompt( directive: &crate::db::models::Directive, existing_steps: &[crate::db::models::DirectiveStep], generation: i32, + goal_history: &[crate::db::models::DirectiveGoalHistory], ) -> String { let mut prompt = String::new(); @@ -854,8 +858,42 @@ fn build_planning_prompt( prompt.push_str(&format!( "⚠️ RE-PLANNING: The GOAL has been updated — you must re-evaluate ALL existing steps.\n\ Previous steps were planned for an earlier version of the goal. Some may no longer be \ - relevant. Review each step below and act according to the instructions per status category.\n\n\ - EXISTING STEPS (generation {}):\n", + relevant. Review each step below and act according to the instructions per status category.\n\n", + )); + + // ── Goal changes section ────────────────────────────────── + if !goal_history.is_empty() { + prompt.push_str("-- GOAL CHANGES --\n"); + prompt.push_str("The goal has been updated. Compare the previous and current goals to understand what changed:\n\n"); + for (i, entry) in goal_history.iter().enumerate() { + if i == 0 { + prompt.push_str(&format!( + "PREVIOUS GOAL (replaced at {}):\n{}\n\n", + entry.created_at.format("%Y-%m-%d %H:%M:%S UTC"), + entry.goal + )); + } else { + prompt.push_str(&format!( + "OLDER GOAL (version from {}):\n{}\n\n", + entry.created_at.format("%Y-%m-%d %H:%M:%S UTC"), + entry.goal + )); + } + } + prompt.push_str(&format!( + "CURRENT GOAL (what you must plan for):\n{}\n\n", + directive.goal + )); + prompt.push_str( + "IMPORTANT: Analyze what CHANGED between the previous goal and the current goal.\n\ + - If the change is minor (e.g., clarification, small addition), try to KEEP existing pending steps and only add/modify what is needed for the delta.\n\ + - If the change is major (e.g., completely different objective), you may need to remove most pending steps and create a fresh plan.\n\ + - Always preserve completed and running steps - they represent work already done.\n\n", + ); + } + + prompt.push_str(&format!( + "EXISTING STEPS (generation {}):\n", generation - 1 )); diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index 929769c..6060171 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -823,29 +823,32 @@ pub async fn update_goal( .into_response(); }; - match repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await { - Ok(Some(directive)) => { - // Clear non-started steps so replanning starts fresh - match repository::clear_pending_directive_steps(pool, id).await { - Ok(count) => { - if count > 0 { - tracing::info!( - directive_id = %id, - removed_steps = count, - "Cleared pending steps after goal update — replanning will generate new steps" - ); - } - } - Err(e) => { - tracing::warn!( - directive_id = %id, - error = %e, - "Failed to clear pending steps after goal update" - ); - } + // Save old goal to history before overwriting (best-effort) + match repository::get_directive_for_owner(pool, auth.owner_id, id).await { + Ok(Some(current)) => { + if let Err(e) = repository::save_directive_goal_history(pool, id, ¤t.goal).await + { + tracing::warn!( + directive_id = %id, + error = %e, + "Failed to save goal history before update — continuing with goal update" + ); } - Json(directive).into_response() } + Ok(None) => { + // Directive not found — update_directive_goal will handle this + } + Err(e) => { + tracing::warn!( + directive_id = %id, + error = %e, + "Failed to fetch current directive for goal history — continuing with goal update" + ); + } + } + + match repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await { + Ok(Some(directive)) => Json(directive).into_response(), Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), |
