2026-07-07 16:10:59 -04:00
|
|
|
import { db } from "@tasks/database";
|
|
|
|
|
import IoRedis from "ioredis";
|
|
|
|
|
|
2026-08-25 17:20:53 -04:00
|
|
|
import { detectStalledRunsForResume } from "./db/convoy.js";
|
2026-07-07 16:10:59 -04:00
|
|
|
import { loadEnv } from "./env.js";
|
2026-08-25 17:20:53 -04:00
|
|
|
import { enqueueJob } from "./queue/redis-queue.js";
|
2026-07-07 16:10:59 -04:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-25 17:20:53 -04:00
|
|
|
const stalled = await detectStalledRunsForResume(db, env.ORCHESTRATOR_STALL_MINUTES);
|
2026-07-07 16:10:59 -04:00
|
|
|
if (stalled.length > 0) {
|
|
|
|
|
console.warn(
|
|
|
|
|
`[orchestrator] stall detector: ${stalled.length} open agent_runs older than ${env.ORCHESTRATOR_STALL_MINUTES}m`,
|
|
|
|
|
);
|
2026-08-25 17:20:53 -04:00
|
|
|
|
|
|
|
|
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}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-07 16:10:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|