diff options
| author | soryu <soryu@soryu.co> | 2026-02-10 23:29:47 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-10 23:29:47 +0000 |
| commit | d9191d4c336daa40b568b640eba92186e9142b53 (patch) | |
| tree | b18dcd581ab60cb65e54dece96fad66b3d8a01d2 | |
| parent | 339c1769379a851c4126021132573bd4b7994cf2 (diff) | |
| download | soryu-makima/makima--add-an-optional-memory-system-for-directiv-3b19fc34.tar.gz soryu-makima/makima--add-an-optional-memory-system-for-directiv-3b19fc34.zip | |
WIP: heartbeat checkpointmakima/makima--add-an-optional-memory-system-for-directiv-3b19fc34
| -rw-r--r-- | makima/migrations/20260211000000_add_directive_memories.sql | 23 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 39 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 69 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 85 |
4 files changed, 206 insertions, 10 deletions
diff --git a/makima/migrations/20260211000000_add_directive_memories.sql b/makima/migrations/20260211000000_add_directive_memories.sql new file mode 100644 index 0000000..b5d4d1f --- /dev/null +++ b/makima/migrations/20260211000000_add_directive_memories.sql @@ -0,0 +1,23 @@ +-- Add memory system for directives. +-- Memories are key insights, decisions, or context that persist across planning/execution cycles. + +-- Add memory_enabled flag to directives +ALTER TABLE directives ADD COLUMN memory_enabled BOOLEAN NOT NULL DEFAULT FALSE; + +-- Create directive_memories table +CREATE TABLE directive_memories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE, + content TEXT NOT NULL, + -- Category: insight, decision, context, error, pattern + category VARCHAR(64) NOT NULL DEFAULT 'context' + CHECK (category IN ('insight', 'decision', 'context', 'error', 'pattern')), + -- Source: planner, executor, user + source VARCHAR(64) NOT NULL DEFAULT 'planner', + -- Optional: which step produced this memory + step_id UUID REFERENCES directive_steps(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_directive_memories_directive_id ON directive_memories(directive_id); +CREATE INDEX idx_directive_memories_category ON directive_memories(directive_id, category); diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 9159fd5..9e263cd 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2713,6 +2713,7 @@ pub struct Directive { pub orchestrator_task_id: Option<Uuid>, pub goal_updated_at: DateTime<Utc>, pub started_at: Option<DateTime<Utc>>, + pub memory_enabled: bool, pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, @@ -2784,6 +2785,8 @@ pub struct CreateDirectiveRequest { pub repository_url: Option<String>, pub local_path: Option<String>, pub base_branch: Option<String>, + #[serde(default)] + pub memory_enabled: bool, } /// Request to update a directive. @@ -2797,6 +2800,7 @@ pub struct UpdateDirectiveRequest { pub local_path: Option<String>, pub base_branch: Option<String>, pub orchestrator_task_id: Option<Uuid>, + pub memory_enabled: Option<bool>, pub version: Option<i32>, } @@ -2833,3 +2837,38 @@ pub struct UpdateDirectiveStepRequest { pub task_id: Option<Uuid>, pub order_index: Option<i32>, } + +/// A memory entry for a directive — persists insights, decisions, and context across cycles. +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveMemory { + pub id: Uuid, + pub directive_id: Uuid, + pub content: String, + /// Category: insight, decision, context, error, pattern + pub category: String, + /// Source: planner, executor, user + pub source: String, + pub step_id: Option<Uuid>, + pub created_at: DateTime<Utc>, +} + +/// Request to create a directive memory. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateDirectiveMemoryRequest { + pub content: String, + #[serde(default = "default_memory_category")] + pub category: String, + #[serde(default = "default_memory_source")] + pub source: String, + pub step_id: Option<Uuid>, +} + +fn default_memory_category() -> String { + "context".to_string() +} + +fn default_memory_source() -> String { + "user".to_string() +} diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 930a73e..c4ee124 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, + CreateDirectiveMemoryRequest, CreateDirectiveRequest, CreateDirectiveStepRequest, + DirectiveMemory, UpdateDirectiveRequest, UpdateDirectiveStepRequest, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, @@ -4926,8 +4926,8 @@ pub async fn create_directive_for_owner( ) -> Result<Directive, sqlx::Error> { sqlx::query_as::<_, Directive>( r#" - INSERT INTO directives (owner_id, title, goal, repository_url, local_path, base_branch) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO directives (owner_id, title, goal, repository_url, local_path, base_branch, memory_enabled) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * "#, ) @@ -4937,6 +4937,7 @@ pub async fn create_directive_for_owner( .bind(&req.repository_url) .bind(&req.local_path) .bind(&req.base_branch) + .bind(req.memory_enabled) .fetch_one(pool) .await } @@ -5040,12 +5041,14 @@ pub async fn update_directive_for_owner( let local_path = req.local_path.as_deref().or(current.local_path.as_deref()); let base_branch = req.base_branch.as_deref().or(current.base_branch.as_deref()); let orchestrator_task_id = req.orchestrator_task_id.or(current.orchestrator_task_id); + let memory_enabled = req.memory_enabled.unwrap_or(current.memory_enabled); let result = sqlx::query_as::<_, Directive>( r#" UPDATE directives SET title = $3, goal = $4, status = $5, repository_url = $6, local_path = $7, - base_branch = $8, orchestrator_task_id = $9, version = version + 1, updated_at = NOW() + base_branch = $8, orchestrator_task_id = $9, memory_enabled = $10, + version = version + 1, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * "#, @@ -5059,6 +5062,7 @@ pub async fn update_directive_for_owner( .bind(local_path) .bind(base_branch) .bind(orchestrator_task_id) + .bind(memory_enabled) .fetch_optional(pool) .await .map_err(RepositoryError::Database)?; @@ -5372,6 +5376,7 @@ pub struct StepForDispatch { pub directive_title: String, pub repository_url: Option<String>, pub base_branch: Option<String>, + pub memory_enabled: bool, } /// Get ready steps that need task dispatch. @@ -5391,7 +5396,8 @@ pub async fn get_ready_steps_for_dispatch( d.owner_id, d.title AS directive_title, d.repository_url, - d.base_branch + d.base_branch, + d.memory_enabled FROM directive_steps ds JOIN directives d ON d.id = ds.directive_id WHERE ds.status = 'ready' @@ -5612,3 +5618,54 @@ pub async fn get_directive_max_generation( .await?; Ok(row.0.unwrap_or(0)) } + +/// List all memories for a directive, ordered by creation time. +pub async fn list_directive_memories( + pool: &PgPool, + directive_id: Uuid, +) -> Result<Vec<DirectiveMemory>, sqlx::Error> { + sqlx::query_as::<_, DirectiveMemory>( + r#" + SELECT * FROM directive_memories + WHERE directive_id = $1 + ORDER BY created_at ASC + "#, + ) + .bind(directive_id) + .fetch_all(pool) + .await +} + +/// Create a new memory entry for a directive. +pub async fn create_directive_memory( + pool: &PgPool, + directive_id: Uuid, + req: CreateDirectiveMemoryRequest, +) -> Result<DirectiveMemory, sqlx::Error> { + sqlx::query_as::<_, DirectiveMemory>( + r#" + INSERT INTO directive_memories (directive_id, content, category, source, step_id) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + "#, + ) + .bind(directive_id) + .bind(&req.content) + .bind(&req.category) + .bind(&req.source) + .bind(req.step_id) + .fetch_one(pool) + .await +} + +/// Delete a memory entry. +pub async fn delete_directive_memory( + pool: &PgPool, + memory_id: Uuid, +) -> Result<bool, sqlx::Error> { + let result = sqlx::query(r#"DELETE FROM directive_memories WHERE id = $1"#) + .bind(memory_id) + .execute(pool) + .await?; + Ok(result.rows_affected() > 0) +} diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 744977e..dded494 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -9,7 +9,7 @@ use sqlx::PgPool; use uuid::Uuid; -use crate::db::models::{CreateTaskRequest, UpdateTaskRequest}; +use crate::db::models::{CreateTaskRequest, DirectiveMemory, UpdateTaskRequest}; use crate::db::repository; use crate::server::state::{DaemonCommand, SharedState}; @@ -43,7 +43,24 @@ impl DirectiveOrchestrator { "Directive needs planning — spawning planning task" ); - let plan = build_planning_prompt(&directive, &[], 1); + // Load memories if memory is enabled for this directive + let memories = if directive.memory_enabled { + match repository::list_directive_memories(&self.pool, directive.id).await { + Ok(m) => m, + Err(e) => { + tracing::warn!( + directive_id = %directive.id, + error = %e, + "Failed to load directive memories for planning — continuing without" + ); + vec![] + } + } + } else { + vec![] + }; + + let plan = build_planning_prompt(&directive, &[], 1, &memories); if let Err(e) = self .spawn_orchestrator_task( @@ -85,17 +102,40 @@ impl DirectiveOrchestrator { .as_deref() .unwrap_or("Execute the step described below."); + // Load memories if memory is enabled for this directive + let memory_context = if step.memory_enabled { + match repository::list_directive_memories(&self.pool, step.directive_id).await { + Ok(memories) if !memories.is_empty() => { + format!("\n\nMEMORY CONTEXT (from previous planning/execution cycles):\n{}\n", + format_memories_for_prompt(&memories)) + } + Ok(_) => String::new(), + Err(e) => { + tracing::warn!( + directive_id = %step.directive_id, + error = %e, + "Failed to load directive memories for execution — continuing without" + ); + String::new() + } + } + } else { + String::new() + }; + let plan = format!( "You are executing a step in directive \"{directive_title}\".\n\n\ STEP: {step_name}\n\ DESCRIPTION: {description}\n\n\ - INSTRUCTIONS:\n{task_plan}\n\n\ + INSTRUCTIONS:\n{task_plan}\n\ + {memory_context}\ When done, the system will automatically mark this step as completed.\n\ If you cannot complete the task, report the failure clearly.", directive_title = step.directive_title, step_name = step.step_name, description = step.step_description.as_deref().unwrap_or("(none)"), task_plan = task_plan, + memory_context = memory_context, ); match self @@ -238,7 +278,24 @@ impl DirectiveOrchestrator { let generation = repository::get_directive_max_generation(&self.pool, directive.id).await? + 1; - let plan = build_planning_prompt(&directive, &existing_steps, generation); + // Load memories if memory is enabled for this directive + let memories = if directive.memory_enabled { + match repository::list_directive_memories(&self.pool, directive.id).await { + Ok(m) => m, + Err(e) => { + tracing::warn!( + directive_id = %directive.id, + error = %e, + "Failed to load directive memories for re-planning — continuing without" + ); + vec![] + } + } + } else { + vec![] + }; + + let plan = build_planning_prompt(&directive, &existing_steps, generation, &memories); if let Err(e) = self .spawn_orchestrator_task( @@ -439,14 +496,34 @@ impl DirectiveOrchestrator { } } +/// Format memory entries into a readable prompt section. +fn format_memories_for_prompt(memories: &[DirectiveMemory]) -> String { + let mut out = String::new(); + for memory in memories { + out.push_str(&format!( + "- [{}] ({}): {}\n", + memory.category, memory.source, memory.content + )); + } + out +} + /// Build the planning prompt for a directive. fn build_planning_prompt( directive: &crate::db::models::Directive, existing_steps: &[crate::db::models::DirectiveStep], generation: i32, + memories: &[DirectiveMemory], ) -> String { let mut prompt = String::new(); + // Include memory context if available + if !memories.is_empty() { + prompt.push_str("MEMORY CONTEXT (insights and decisions from previous cycles):\n"); + prompt.push_str(&format_memories_for_prompt(memories)); + prompt.push_str("\nUse these memories to inform your planning. Avoid repeating past mistakes and build on prior insights.\n\n"); + } + if !existing_steps.is_empty() { prompt.push_str(&format!( "EXISTING STEPS (generation {}):\n", |
