summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-10 23:29:47 +0000
committersoryu <soryu@soryu.co>2026-02-10 23:29:47 +0000
commitd9191d4c336daa40b568b640eba92186e9142b53 (patch)
treeb18dcd581ab60cb65e54dece96fad66b3d8a01d2
parent339c1769379a851c4126021132573bd4b7994cf2 (diff)
downloadsoryu-makima/makima--add-an-optional-memory-system-for-directiv-3b19fc34.tar.gz
soryu-makima/makima--add-an-optional-memory-system-for-directiv-3b19fc34.zip
-rw-r--r--makima/migrations/20260211000000_add_directive_memories.sql23
-rw-r--r--makima/src/db/models.rs39
-rw-r--r--makima/src/db/repository.rs69
-rw-r--r--makima/src/orchestration/directive.rs85
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",