From 41a24585e8c771132b47b28ab1b5869040744af0 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 14 Feb 2026 02:44:57 +0000 Subject: WIP: heartbeat checkpoint --- .../src/components/directives/DirectiveDetail.tsx | 131 ++++++++++++++++++++- makima/frontend/src/lib/api.ts | 10 ++ makima/frontend/src/routes/directives.tsx | 3 +- makima/src/orchestration/directive.rs | 10 ++ makima/src/server/handlers/mesh_supervisor.rs | 11 +- 5 files changed, 159 insertions(+), 6 deletions(-) diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index b73463d..5128c26 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -1,8 +1,9 @@ import { useState, useMemo, useEffect, useRef } from "react"; -import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api"; +import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest } from "../../lib/api"; import { DirectiveDAG } from "./DirectiveDAG"; import { DirectiveLogStream } from "./DirectiveLogStream"; import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription"; +import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext"; const STATUS_BADGE: Record = { draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" }, @@ -21,6 +22,7 @@ interface DirectiveDetailProps { onFailStep: (stepId: string) => void; onSkipStep: (stepId: string) => void; onUpdateGoal: (goal: string) => void; + onUpdate: (req: UpdateDirectiveRequest) => void; onDelete: () => void; onRefresh: () => void; onCleanupTasks: () => void; @@ -35,6 +37,7 @@ export function DirectiveDetail({ onFailStep, onSkipStep, onUpdateGoal, + onUpdate, onDelete, onRefresh, onCleanupTasks, @@ -53,6 +56,24 @@ export function DirectiveDetail({ const terminalStatuses = new Set(["completed", "failed", "skipped"]); const hasTerminalTasks = directive.steps.some((s) => s.taskId && terminalStatuses.has(s.status)); + // Get pending questions for this directive's tasks + const { pendingQuestions, submitAnswer } = useSupervisorQuestions(); + const directiveTaskIds = useMemo(() => { + const ids = new Set(); + if (directive.orchestratorTaskId) ids.add(directive.orchestratorTaskId); + for (const step of directive.steps) { + if (step.taskId) ids.add(step.taskId); + } + return ids; + }, [directive.orchestratorTaskId, directive.steps]); + + const directiveQuestions = useMemo( + () => pendingQuestions.filter((q) => + q.directiveId === directive.id || directiveTaskIds.has(q.taskId) + ), + [pendingQuestions, directive.id, directiveTaskIds] + ); + // Build task map from directive steps and orchestrator // Derive a stable key from the actual task IDs to avoid recreating the map on every poll const taskMapKey = useMemo(() => { @@ -149,6 +170,26 @@ export function DirectiveDetail({ )} + {/* Reconcile mode toggle */} +
+ + + {directive.reconcileMode + ? "Questions pause execution" + : "Questions timeout after 30s"} + +
+ {/* Orchestrator planning indicator */} {directive.orchestratorTaskId && (
@@ -199,6 +240,20 @@ export function DirectiveDetail({
)} + {/* Pending Questions */} + {directiveQuestions.length > 0 && ( +
+ {directiveQuestions.map((q) => ( + submitAnswer(q.questionId, response)} + /> + ))} +
+ )} + {/* Controls */}
{(directive.status === "draft" || directive.status === "paused") && ( @@ -342,3 +397,77 @@ export function DirectiveDetail({
); } + +/** Inline question card for directive pending questions */ +function DirectiveQuestionCard({ + question, + taskName, + onAnswer, +}: { + question: { questionId: string; question: string; choices: string[]; context: string | null }; + taskName: string; + onAnswer: (response: string) => void; +}) { + const [customResponse, setCustomResponse] = useState(""); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async (response: string) => { + setSubmitting(true); + await onAnswer(response); + setSubmitting(false); + }; + + return ( +
+
+ + + Question from {taskName} + +
+

{question.question}

+ {question.context && ( +

{question.context}

+ )} + {question.choices.length > 0 ? ( +
+ {question.choices.map((choice) => ( + + ))} +
+ ) : ( +
+ setCustomResponse(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && customResponse.trim()) { + handleSubmit(customResponse.trim()); + } + }} + placeholder="Type your answer..." + className="flex-1 bg-[#0a0618] border border-purple-900/50 rounded px-2 py-0.5 text-[10px] font-mono text-white placeholder:text-[#445566]" + disabled={submitting} + /> + +
+ )} +
+ ); +} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 480041c..353faad 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -2241,6 +2241,8 @@ export interface PendingQuestion { questionId: string; taskId: string; contractId: string; + /** Directive this question relates to (if from a directive task) */ + directiveId?: string | null; question: string; choices: string[]; context: string | null; @@ -3025,6 +3027,8 @@ export interface Directive { completionTaskId: string | null; /** Whether the memory system is enabled for this directive */ memoryEnabled: boolean; + /** Whether questions pause execution indefinitely until answered */ + reconcileMode: boolean; goalUpdatedAt: string; startedAt: string | null; version: number; @@ -3064,6 +3068,8 @@ export interface DirectiveSummary { completionTaskId: string | null; /** Whether the memory system is enabled for this directive */ memoryEnabled: boolean; + /** Whether questions pause execution indefinitely until answered */ + reconcileMode: boolean; version: number; createdAt: string; updatedAt: string; @@ -3086,6 +3092,8 @@ export interface CreateDirectiveRequest { baseBranch?: string; /** Enable the memory system for this directive (default: false) */ memoryEnabled?: boolean; + /** Whether questions pause execution indefinitely until answered (default: false) */ + reconcileMode?: boolean; } export interface UpdateDirectiveRequest { @@ -3098,6 +3106,8 @@ export interface UpdateDirectiveRequest { orchestratorTaskId?: string; /** Enable or disable the memory system for this directive */ memoryEnabled?: boolean; + /** Whether questions pause execution indefinitely until answered */ + reconcileMode?: boolean; version?: number; } diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx index ca4437c..643cfee 100644 --- a/makima/frontend/src/routes/directives.tsx +++ b/makima/frontend/src/routes/directives.tsx @@ -12,7 +12,7 @@ export default function DirectivesPage() { const navigate = useNavigate(); const { id: selectedId } = useParams<{ id: string }>(); const { directives, loading: listLoading, create, remove } = useDirectives(); - const { directive, refresh: refreshDetail, start, pause, advance, completeStep, failStep, skipStep, updateGoal, cleanupTasks } = useDirective(selectedId); + const { directive, refresh: refreshDetail, update, start, pause, advance, completeStep, failStep, skipStep, updateGoal, cleanupTasks } = useDirective(selectedId); const [showCreate, setShowCreate] = useState(false); const [newTitle, setNewTitle] = useState(""); @@ -207,6 +207,7 @@ export default function DirectivesPage() { onFailStep={failStep} onSkipStep={skipStep} onUpdateGoal={updateGoal} + onUpdate={update} onDelete={handleDelete} onRefresh={refreshDetail} onCleanupTasks={cleanupTasks} diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index ea8009d..6d5d63d 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -248,6 +248,16 @@ impl DirectiveOrchestrator { .await?; repository::check_directive_idle(&self.pool, step.directive_id).await?; } + "paused" => { + // Task is paused (e.g., waiting for user answer in reconcile mode) + // Keep step in running status — task will auto-resume when answered + tracing::debug!( + step_id = %step.step_id, + directive_id = %step.directive_id, + task_id = %step.task_id, + "Step task paused (waiting for user response) — keeping step running" + ); + } _ => { // Still running — do nothing } diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 7d4ab46..90c6dc7 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -260,11 +260,11 @@ async fn verify_supervisor_auth( ) })?; - // Verify task is a supervisor - if !task.is_supervisor { + // Verify task is a supervisor or a directive task + if !task.is_supervisor && task.directive_id.is_none() { return Err(( StatusCode::FORBIDDEN, - Json(ApiError::new("NOT_SUPERVISOR", "Only supervisor tasks can use these endpoints")), + Json(ApiError::new("NOT_SUPERVISOR", "Only supervisor or directive tasks can use these endpoints")), )); } @@ -1879,7 +1879,10 @@ pub async fn ask_question( state.remove_pending_question(question_id); // Clear pending question from supervisor state on timeout (Task 3.3) - clear_pending_question(pool, contract_id, question_id).await; + // Skip for directive context — no supervisor_states for directives + if let Some(cid) = contract_id { + clear_pending_question(pool, cid, question_id).await; + } return ( StatusCode::REQUEST_TIMEOUT, -- cgit v1.2.3