- MCP create-backlog-item for natural language Plan/Epic/Task authoring - Worker non-code: optional worktree + per-harness allowlist + doc_review→published - Harness registry: harness_providers/role_harness_routes, WorkerAdapter (minimal|crewai|langgraph|remote-ag-ui), AG-UI POST /api/ag-ui, Settings → Harnesses with Test - Plans tree: backlog.listTree + createPlan/Epic/Task + Start→Convoy, web CRUD without CLI - Fleet: /fleet rollup across workspaces + SSE /api/fleet/events Decision: docs/decisions/2026-08-25-second-brain-os-pivot.md Convoy: .convoys/second-brain-os-v01 Co-authored-by: orchestrator
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import { db } from "@tasks/database";
|
|
import IoRedis from "ioredis";
|
|
|
|
import { detectStalledRunsForResume } from "./db/convoy.js";
|
|
import { loadEnv } from "./env.js";
|
|
import { enqueueJob } from "./queue/redis-queue.js";
|
|
import { runOrchestratorTick } from "./tick.js";
|
|
|
|
async function main(): Promise<void> {
|
|
const env = loadEnv();
|
|
const redis = env.REDIS_URL ? new IoRedis(env.REDIS_URL) : null;
|
|
|
|
if (env.ORCHESTRATOR_DRY_RUN) {
|
|
console.info("[orchestrator] DRY-RUN mode — jobs will be logged, not enqueued");
|
|
} else if (!redis) {
|
|
console.warn("[orchestrator] REDIS_URL not set — running in DRY-RUN mode");
|
|
}
|
|
|
|
console.info(`[orchestrator] polling every ${env.ORCHESTRATOR_POLL_MS}ms`);
|
|
|
|
const loop = async () => {
|
|
try {
|
|
const results = await runOrchestratorTick(db, redis, {
|
|
...env,
|
|
ORCHESTRATOR_DRY_RUN: env.ORCHESTRATOR_DRY_RUN || !redis,
|
|
});
|
|
|
|
const stalled = await detectStalledRunsForResume(db, env.ORCHESTRATOR_STALL_MINUTES);
|
|
if (stalled.length > 0) {
|
|
console.warn(
|
|
`[orchestrator] stall detector: ${stalled.length} open agent_runs older than ${env.ORCHESTRATOR_STALL_MINUTES}m`,
|
|
);
|
|
|
|
for (const run of stalled) {
|
|
if (env.ORCHESTRATOR_DRY_RUN || !redis) {
|
|
console.info(
|
|
`[orchestrator] DRY-RUN resume_from_checkpoint convoy=${run.convoyId} role=${run.roleId} run=${run.agentRunId}`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const job = await enqueueJob(redis, {
|
|
type: "role_dispatch",
|
|
workspaceId: run.workspaceId,
|
|
convoyId: run.convoyId,
|
|
roleId: run.roleId,
|
|
briefId: run.briefId,
|
|
resumeFromCheckpoint: true,
|
|
});
|
|
console.info(
|
|
`[orchestrator] re-dispatched job=${job.id} resume_from_checkpoint run=${run.agentRunId}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (results.length === 0) {
|
|
console.info("[orchestrator] no active convoys");
|
|
}
|
|
} catch (error) {
|
|
console.error("[orchestrator] tick failed:", error);
|
|
}
|
|
};
|
|
|
|
await loop();
|
|
setInterval(loop, env.ORCHESTRATOR_POLL_MS);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error("[orchestrator] fatal:", error);
|
|
process.exit(1);
|
|
});
|