From 355f10964c4dbec24a244a00caba5c17ed23fc65 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 12 Feb 2026 02:29:45 +0000 Subject: makima: Add an optional memory system for directives (#59) * feat: makima: Add an optional memory system for directives: Add directive_memories database table and migration * feat: makima: Add an optional memory system for directives: Update directive skill documentation with memory commands * feat: makima: Add an optional memory system for directives: Add repository functions for directive memory CRUD * feat: makima: Add an optional memory system for directives: Add frontend API functions and types for directive memory * feat: makima: Add an optional memory system for directives: Add Rust models for directive memory * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: makima: Add an optional memory system for directives: Add memory panel to frontend DirectiveDetail component * Merge remote-tracking branch 'origin/makima/makima--add-an-optional-memory-system-for-directiv-5de1e06d' into combined branch * Merge remote-tracking branch 'origin/makima/makima--add-an-optional-memory-system-for-directiv-c8298c6c' into combined branch * feat: makima: Add an optional memory system for directives: Create useMultiTaskSubscription hook for multi-output WebSocket streaming * feat: makima: Add an optional memory system for directives: Create DirectiveLogStream component for stern-like multi-task output viewing * feat: makima: Add an optional memory system for directives: Integrate log stream panel into directive detail page --- makima/src/db/models.rs | 47 ++++++++++++++ makima/src/db/repository.rs | 146 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 186 insertions(+), 7 deletions(-) (limited to 'makima/src/db') diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 542339f..169f468 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2714,8 +2714,10 @@ pub struct Directive { pub pr_url: Option, pub pr_branch: Option, pub completion_task_id: Option, + pub memory_enabled: bool, pub goal_updated_at: DateTime, pub started_at: Option>, + pub memory_enabled: bool, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, @@ -2763,6 +2765,7 @@ pub struct DirectiveSummary { pub orchestrator_task_id: Option, pub pr_url: Option, pub completion_task_id: Option, + pub memory_enabled: bool, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, @@ -2789,6 +2792,8 @@ pub struct CreateDirectiveRequest { pub repository_url: Option, pub local_path: Option, pub base_branch: Option, + #[serde(default)] + pub memory_enabled: bool, } /// Request to update a directive. @@ -2804,6 +2809,7 @@ pub struct UpdateDirectiveRequest { pub orchestrator_task_id: Option, pub pr_url: Option, pub pr_branch: Option, + pub memory_enabled: Option, pub version: Option, } @@ -2840,3 +2846,44 @@ pub struct UpdateDirectiveStepRequest { pub task_id: Option, pub order_index: Option, } + +// ============================================================================= +// Directive Memory Types +// ============================================================================= + +/// A memory entry for a directive — key-value context that persists across tasks. +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveMemory { + pub id: Uuid, + pub directive_id: Uuid, + pub key: String, + pub value: String, + pub category: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// Request to set a memory entry (upsert by key). +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SetDirectiveMemoryRequest { + pub key: String, + pub value: String, + pub category: Option, +} + +/// Request to batch set memory entries. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct BatchSetDirectiveMemoryRequest { + pub entries: Vec, +} + +/// Response for listing memories. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveMemoryListResponse { + pub memories: Vec, + pub total: i64, +} diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 7afbeea..95460f7 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -11,9 +11,10 @@ use super::models::{ ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot, CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, - DeliverableDefinition, Directive, DirectiveStep, DirectiveSummary, + DeliverableDefinition, Directive, DirectiveMemory, DirectiveStep, DirectiveSummary, CreateDirectiveRequest, CreateDirectiveStepRequest, UpdateDirectiveRequest, - UpdateDirectiveStepRequest, + UpdateDirectiveStepRequest, SetDirectiveMemoryRequest, + BatchSetDirectiveMemoryRequest, DirectiveMemoryListResponse, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, @@ -4929,8 +4930,8 @@ pub async fn create_directive_for_owner( ) -> Result { 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 * "#, ) @@ -4940,6 +4941,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 } @@ -4992,7 +4994,7 @@ pub async fn list_directives_for_owner( SELECT d.id, d.owner_id, d.title, d.goal, d.status, d.repository_url, d.orchestrator_task_id, d.pr_url, d.completion_task_id, - d.version, d.created_at, d.updated_at, + d.memory_enabled, d.version, d.created_at, d.updated_at, COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id), 0) as total_steps, COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'completed'), 0) as completed_steps, COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'running'), 0) as running_steps, @@ -5046,12 +5048,13 @@ pub async fn update_directive_for_owner( let orchestrator_task_id = req.orchestrator_task_id.or(current.orchestrator_task_id); let pr_url = req.pr_url.as_deref().or(current.pr_url.as_deref()); let pr_branch = req.pr_branch.as_deref().or(current.pr_branch.as_deref()); + 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, pr_url = $10, pr_branch = $11, + base_branch = $8, orchestrator_task_id = $9, pr_url = $10, pr_branch = $11, memory_enabled = $12, version = version + 1, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * @@ -5068,6 +5071,7 @@ pub async fn update_directive_for_owner( .bind(orchestrator_task_id) .bind(pr_url) .bind(pr_branch) + .bind(memory_enabled) .fetch_optional(pool) .await .map_err(RepositoryError::Database)?; @@ -5500,6 +5504,7 @@ pub struct StepForDispatch { pub directive_title: String, pub repository_url: Option, pub base_branch: Option, + pub memory_enabled: bool, } /// Get ready steps that need task dispatch. @@ -5519,7 +5524,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' @@ -5740,3 +5746,129 @@ pub async fn get_directive_max_generation( .await?; Ok(row.0.unwrap_or(0)) } + +// ============================================================================= +// Directive Memory CRUD +// ============================================================================= + +/// List all memories for a directive, optionally filtered by category. +pub async fn list_directive_memories( + pool: &PgPool, + directive_id: Uuid, + category: Option<&str>, +) -> Result, sqlx::Error> { + match category { + Some(cat) => { + sqlx::query_as::<_, DirectiveMemory>( + r#" + SELECT * FROM directive_memories + WHERE directive_id = $1 AND category = $2 + ORDER BY key + "#, + ) + .bind(directive_id) + .bind(cat) + .fetch_all(pool) + .await + } + None => { + sqlx::query_as::<_, DirectiveMemory>( + r#" + SELECT * FROM directive_memories + WHERE directive_id = $1 + ORDER BY key + "#, + ) + .bind(directive_id) + .fetch_all(pool) + .await + } + } +} + +/// Get a single memory entry by directive ID and key. +pub async fn get_directive_memory( + pool: &PgPool, + directive_id: Uuid, + key: &str, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, DirectiveMemory>( + r#" + SELECT * FROM directive_memories + WHERE directive_id = $1 AND key = $2 + "#, + ) + .bind(directive_id) + .bind(key) + .fetch_optional(pool) + .await +} + +/// Set (upsert) a memory entry for a directive. +pub async fn set_directive_memory( + pool: &PgPool, + directive_id: Uuid, + req: &SetDirectiveMemoryRequest, +) -> Result { + sqlx::query_as::<_, DirectiveMemory>( + r#" + INSERT INTO directive_memories (directive_id, key, value, category) + VALUES ($1, $2, $3, $4) + ON CONFLICT (directive_id, key) + DO UPDATE SET value = EXCLUDED.value, + category = EXCLUDED.category, + updated_at = NOW() + RETURNING * + "#, + ) + .bind(directive_id) + .bind(&req.key) + .bind(&req.value) + .bind(&req.category) + .fetch_one(pool) + .await +} + +/// Batch set memory entries for a directive. +pub async fn batch_set_directive_memories( + pool: &PgPool, + directive_id: Uuid, + memories: &[SetDirectiveMemoryRequest], +) -> Result, sqlx::Error> { + let mut results = Vec::with_capacity(memories.len()); + for mem in memories { + let result = set_directive_memory(pool, directive_id, mem).await?; + results.push(result); + } + Ok(results) +} + +/// Delete a single memory entry by key. +pub async fn delete_directive_memory( + pool: &PgPool, + directive_id: Uuid, + key: &str, +) -> Result { + let result = sqlx::query( + r#"DELETE FROM directive_memories WHERE directive_id = $1 AND key = $2"#, + ) + .bind(directive_id) + .bind(key) + .execute(pool) + .await?; + Ok(result.rows_affected() > 0) +} + +/// Delete all memory entries for a directive. +pub async fn clear_directive_memories( + pool: &PgPool, + directive_id: Uuid, +) -> Result { + let result = sqlx::query( + r#"DELETE FROM directive_memories WHERE directive_id = $1"#, + ) + .bind(directive_id) + .execute(pool) + .await?; + Ok(result.rows_affected()) +} -- cgit v1.2.3