Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 116 additions & 109 deletions optimus-plugin/dist/http-runtime.js

Large diffs are not rendered by default.

498 changes: 253 additions & 245 deletions optimus-plugin/dist/mcp-server.js

Large diffs are not rendered by default.

160 changes: 81 additions & 79 deletions optimus-plugin/dist/runtime-cli.js

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions src/mcp/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ["workspace_path", "task_id", "reason"],
}
},
{
name: "scheduler_resume_context",
description: "Return a prompt-friendly scheduler context packet so the master agent can resume or take over a task from durable task_events instead of transient chat memory.",
inputSchema: {
type: "object",
properties: {
workspace_path: { type: "string", description: "Absolute path to the project workspace root." },
task_id: { type: "string", description: "Scheduler task ID to recover context for." },
},
required: ["workspace_path", "task_id"],
}
},
{
name: "scheduler_promote_memory",
description: "Explicitly promote a selected reusable lesson from a scheduler task into long-term project or role memory. This never copies the full scheduler event log automatically.",
inputSchema: {
type: "object",
properties: {
workspace_path: { type: "string", description: "Absolute path to the project workspace root." },
task_id: { type: "string", description: "Scheduler task ID that produced the lesson." },
level: { type: "string", enum: ["project", "role"], description: "Long-term memory scope." },
category: { type: "string", description: "Memory category, e.g. architecture-decision or workflow." },
tags: { type: "array", items: { type: "string" }, description: "Tags for selective memory loading." },
content: { type: "string", description: "The concise reusable lesson to store. Do not pass raw task logs." },
role: { type: "string", description: "Role name for role-level memory. Defaults to task required_capability." },
},
required: ["workspace_path", "task_id", "level", "category", "content"],
}
},
{
name: "write_blackboard_artifact",
description: "Write a file to the .optimus/ blackboard directory. Only paths within .optimus/ are allowed. Use this for specs (problem/proposal/solution), task descriptions, reports, and other orchestration artifacts. artifact_path is relative to the .optimus/ directory (do NOT include the .optimus/ prefix). Routing: specs/{date}-{topic}/ for Problem-First lifecycle, tasks/ for issue bindings, reports/ for analysis, results/ for task output.",
Expand Down Expand Up @@ -1471,6 +1500,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return { content: [{ type: "text", text: `Scheduler task yielded: \`${task.id}\` [${task.status}]\n\nYield preserves scheduler state and does not stop running sub-agents.` }] };
}

if (request.params.name === "scheduler_resume_context") {
const { workspace_path, task_id } = request.params.arguments as any;
requireParams("scheduler_resume_context", request.params.arguments as any, ["workspace_path", "task_id"]);
const scheduler = new MasterScheduler(workspace_path);
const resumeContext = scheduler.getResumeContext(task_id);
if (!resumeContext.task) {
return { content: [{ type: "text", text: `Scheduler task ${task_id} not found.` }] };
}
return {
content: [{
type: "text",
text: [
`Scheduler resume context`,
``,
`**Suggested Next Action**: ${resumeContext.suggested_next_action}`,
``,
resumeContext.context,
].filter(Boolean).join('\n')
}]
};
}

if (request.params.name === "scheduler_promote_memory") {
const { workspace_path, task_id, level, category, content, role } = request.params.arguments as any;
requireParams("scheduler_promote_memory", request.params.arguments as any, ["workspace_path", "task_id", "level", "category", "content"]);
if (!["project", "role"].includes(level)) {
throw new McpError(ErrorCode.InvalidParams, "Invalid arguments for scheduler_promote_memory: level must be project or role.");
}
const tags = Array.isArray((request.params.arguments as any).tags)
? (request.params.arguments as any).tags.filter((item: unknown): item is string => typeof item === 'string')
: [];
const scheduler = new MasterScheduler(workspace_path);
const task = scheduler.promoteTaskMemory(task_id, { level, category, tags, content, role });
if (!task) {
return { content: [{ type: "text", text: `Scheduler task ${task_id} not found.` }] };
}
return { content: [{ type: "text", text: `Scheduler task memory promoted: \`${task.id}\`\n\nOnly the explicit lesson was written to long-term ${level} memory; scheduler events were not copied automatically.` }] };
}

if (request.params.name === "check_task_status") {
let { taskId, workspace_path } = request.params.arguments as any;
requireParams("check_task_status", request.params.arguments as any, ["taskId", "workspace_path"]);
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/agentRuntimeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ function createRuntimeRecord(request: AgentRuntimeRequest, runId: string, traceI
instructions: request.instructions,
input: request.input,
context_files: request.context_files,
scheduler_context: request.scheduler_context,
runtime_policy: request.runtime_policy
},
history: [
Expand Down Expand Up @@ -188,7 +189,8 @@ export function normalizeRuntimeRequest(args: any): AgentRuntimeRequest {
role_engine: args.role_engine,
role_model: args.role_model,
agent_id: args.agent_id,
context_files: Array.isArray(args.context_files) ? args.context_files : undefined
context_files: Array.isArray(args.context_files) ? args.context_files : undefined,
scheduler_context: typeof args.scheduler_context === 'string' ? args.scheduler_context : undefined
};
}

Expand Down
53 changes: 52 additions & 1 deletion src/runtime/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
* POST /api/v1/scheduler/tasks/:id/checkpoint — Checkpoint master-agent progress
* POST /api/v1/scheduler/tasks/:id/handoff — Handoff master work to a sub-agent
* POST /api/v1/scheduler/tasks/:id/yield — Yield master focus without stopping workers
* GET /api/v1/scheduler/tasks/:id/resume-context — Get prompt-friendly resume context
* POST /api/v1/scheduler/tasks/:id/promote-memory — Explicitly promote a lesson to long-term memory
* GET /api/v1/health — Health check
*
* Auto-scaling: when at capacity, spawns overflow instances on adjacent ports.
Expand Down Expand Up @@ -749,6 +751,55 @@ async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse
return;
}

// GET /api/v1/scheduler/tasks/:id/resume-context — prompt-friendly task recovery context
if ((params = matchRoute(method, url, '/api/v1/scheduler/tasks/:id/resume-context', 'GET'))) {
const workspacePath = resolveWorkspaceFromHeader(defaultWorkspacePath, req.headers['x-optimus-workspace'] as string | undefined);
const scheduler = createHttpScheduler(workspacePath);
const resumeContext = scheduler.getResumeContext(params.id);
if (!resumeContext.task) {
sendError(res, 404, 'task_not_found', `Scheduler task '${params.id}' was not found.`);
return;
}
sendJson(res, 200, {
scheduler_scope: 'optimus_application_layer',
note: 'Prompt-friendly scheduler context for master recovery; not long-term memory.',
resume_context: resumeContext,
});
return;
}

// POST /api/v1/scheduler/tasks/:id/promote-memory — explicit long-term memory promotion
if ((params = matchRoute(method, url, '/api/v1/scheduler/tasks/:id/promote-memory', 'POST'))) {
const body = parseJsonBody(await readBody(req));
const workspacePath = resolveWorkspaceFromBody(defaultWorkspacePath, body.workspace_path);
if (body.level !== 'project' && body.level !== 'role') {
throw new RuntimeError('Invalid memory level', 'invalid_params', 400, 'Use level: "project" or "role".');
}
if (typeof body.category !== 'string' || !body.category.trim() || typeof body.content !== 'string' || !body.content.trim()) {
throw new RuntimeError('Missing required fields: category, content', 'missing_params', 400,
'Include category and content in the JSON body. Do not pass raw scheduler event logs.'
);
}
const scheduler = createHttpScheduler(workspacePath);
const task = scheduler.promoteTaskMemory(params.id, {
level: body.level,
category: body.category,
tags: Array.isArray(body.tags) ? body.tags.filter((item: unknown): item is string => typeof item === 'string') : [],
content: body.content,
role: typeof body.role === 'string' ? body.role : undefined,
});
if (!task) {
sendError(res, 404, 'task_not_found', `Scheduler task '${params.id}' was not found.`);
return;
}
sendJson(res, 200, {
scheduler_scope: 'optimus_application_layer',
note: 'Only the explicit lesson was promoted to long-term memory; scheduler events were not copied automatically.',
task,
});
return;
}

// ─── v2 Generic API (no Optimus orchestration) ───

// GET /api/v2/health
Expand Down Expand Up @@ -854,7 +905,7 @@ async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse

// 404
sendError(res, 404, 'not_found', `Route not found: ${method} ${url}`,
'Valid endpoints: POST /api/v1/agent/run, POST /api/v1/agent/start, GET /api/v1/agent/runs/:id, POST /api/v1/agent/runs/:id/resume, POST /api/v1/agent/runs/:id/cancel, POST /api/v1/scheduler/inbox, POST /api/v1/scheduler/tick, GET /api/v1/scheduler/tasks, GET /api/v1/scheduler/tasks/:id, POST /api/v1/scheduler/tasks/:id/cancel, POST /api/v1/scheduler/tasks/:id/pause, POST /api/v1/scheduler/tasks/:id/resume, POST /api/v1/scheduler/tasks/:id/reassign, POST /api/v1/scheduler/tasks/:id/checkpoint, POST /api/v1/scheduler/tasks/:id/handoff, POST /api/v1/scheduler/tasks/:id/yield, GET /api/v1/health, POST /api/v2/agent/run, POST /api/v2/agent/start, GET /api/v2/agent/runs/:id, POST /api/v2/agent/runs/:id/cancel, GET /api/v2/health'
'Valid endpoints: POST /api/v1/agent/run, POST /api/v1/agent/start, GET /api/v1/agent/runs/:id, POST /api/v1/agent/runs/:id/resume, POST /api/v1/agent/runs/:id/cancel, POST /api/v1/scheduler/inbox, POST /api/v1/scheduler/tick, GET /api/v1/scheduler/tasks, GET /api/v1/scheduler/tasks/:id, GET /api/v1/scheduler/tasks/:id/resume-context, POST /api/v1/scheduler/tasks/:id/cancel, POST /api/v1/scheduler/tasks/:id/pause, POST /api/v1/scheduler/tasks/:id/resume, POST /api/v1/scheduler/tasks/:id/reassign, POST /api/v1/scheduler/tasks/:id/checkpoint, POST /api/v1/scheduler/tasks/:id/handoff, POST /api/v1/scheduler/tasks/:id/yield, POST /api/v1/scheduler/tasks/:id/promote-memory, GET /api/v1/health, POST /api/v2/agent/run, POST /api/v2/agent/start, GET /api/v2/agent/runs/:id, POST /api/v2/agent/runs/:id/cancel, GET /api/v2/health'
);
}

Expand Down
80 changes: 80 additions & 0 deletions src/runtime/masterScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import {
} from './schedulerStore';
import { startRun, getRunStatus, cancelRun } from './agentRuntimeService';
import { TaskManifestManager } from '../managers/TaskManifestManager';
import {
buildSchedulerContextPacket,
formatSchedulerContextForPrompt,
} from './schedulerContext';
import { buildMemoryEntry } from '../managers/MemoryManager';
import { detectWorktreeContext } from '../utils/worktree';
import fs from 'fs';
import path from 'path';

export const MASTER_SCHEDULER_PROTOCOL = [
'Application-layer scheduler: this does not intercept or replace Copilot core turn scheduling.',
Expand Down Expand Up @@ -97,6 +105,20 @@ export interface SchedulerYieldOptions {
checkpoint?: SchedulerCheckpoint;
}

export interface SchedulerResumeContext {
task?: SchedulerTask;
context?: string;
suggested_next_action: 'continue_as_master' | 'handoff_to_sub_agent' | 'tick_scheduler' | 'ask_user' | 'task_not_found';
}

export interface SchedulerMemoryPromotion {
level: 'project' | 'role';
category: string;
tags: string[];
content: string;
role?: string;
}

const DEFAULT_WORKER_ROLES: Record<'research_worker' | 'coding_worker', string> = {
research_worker: 'researcher',
coding_worker: 'developer',
Expand Down Expand Up @@ -260,6 +282,59 @@ export class MasterScheduler {
};
}

getResumeContext(taskId: string): SchedulerResumeContext {
const packet = buildSchedulerContextPacket(this.workspacePath, taskId);
if (!packet) {
return { suggested_next_action: 'task_not_found' };
}
const context = formatSchedulerContextForPrompt(packet);
let suggested: SchedulerResumeContext['suggested_next_action'] = 'continue_as_master';
if (packet.task.status === 'ready') suggested = 'tick_scheduler';
if (packet.latest_handoff) suggested = 'handoff_to_sub_agent';
if (packet.task.status === 'blocked' && packet.task.blocking_reason) suggested = 'ask_user';
return { task: packet.task, context, suggested_next_action: suggested };
}

promoteTaskMemory(taskId: string, promotion: SchedulerMemoryPromotion): SchedulerTask | undefined {
const task = this.store.getTask(taskId);
if (!task) return undefined;
const memoryFile = this.getPromotionMemoryPath(promotion.level, promotion.role || task.required_capability);
fs.mkdirSync(path.dirname(memoryFile), { recursive: true });
const entry = buildMemoryEntry({
level: promotion.level,
category: promotion.category,
tags: promotion.tags,
content: promotion.content,
author: 'scheduler-memory-bridge',
});
fs.appendFileSync(memoryFile, entry, 'utf8');
this.store.appendTaskEvent({
task_id: task.id,
event_type: 'task_memory_promoted',
payload: {
level: promotion.level,
category: promotion.category,
tags: promotion.tags,
role: promotion.role,
memory_file: memoryFile,
},
});
return task;
}

private getPromotionMemoryPath(level: 'project' | 'role', role: string): string {
const ctx = detectWorktreeContext(this.workspacePath);
const memoryRoot = path.join(ctx.mainRoot, '.optimus', 'memory');
if (level === 'project') {
return path.join(memoryRoot, 'continuous-memory.md');
}
const sanitizedRole = role.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 100);
if (!sanitizedRole) {
throw new Error(`Invalid role name for memory promotion: '${role}'`);
}
return path.join(memoryRoot, 'roles', `${sanitizedRole}.md`);
}

checkpointTask(taskId: string, checkpoint: SchedulerCheckpoint): SchedulerTask | undefined {
const task = this.store.getTask(taskId);
if (!task) return undefined;
Expand Down Expand Up @@ -823,6 +898,10 @@ export class MasterScheduler {
}
try {
const role = this.resolveRoleForTask(candidate);
const schedulerContextPacket = buildSchedulerContextPacket(this.workspacePath, candidate.id);
const schedulerContext = schedulerContextPacket
? formatSchedulerContextForPrompt(schedulerContextPacket)
: undefined;
const envelope = startRun({
role,
workspace_path: this.workspacePath,
Expand All @@ -835,6 +914,7 @@ export class MasterScheduler {
].filter(Boolean).join('\n'),
context_files: candidate.affected_files,
agent_id: candidate.assigned_agent_id,
scheduler_context: schedulerContext,
});
if (acquiredWorkerSlot) {
this.onWorkerRunStarted?.(envelope.run_id, this.workspacePath);
Expand Down
Loading