Files
clawd/antfarm/src/installer/agent-cron.ts.BACKUP
Echo dc64d18224 fix: convert antfarm from broken submodule to regular directory
Fixes Gitea 500 error caused by invalid submodule reference.
Converted antfarm from pseudo-submodule (missing .gitmodules) to
regular directory with all source files.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 16:03:37 +00:00

129 lines
4.6 KiB
Plaintext

import { createAgentCronJob, deleteAgentCronJobs, listCronJobs, checkCronToolAvailable } from "./gateway-api.js";
import type { WorkflowSpec } from "./types.js";
import { resolveAntfarmCli } from "./paths.js";
import { getDb } from "../db.js";
const DEFAULT_EVERY_MS = 300_000; // 5 minutes
function buildAgentPrompt(workflowId: string, agentId: string): string {
const fullAgentId = `${workflowId}/${agentId}`;
const cli = resolveAntfarmCli();
return `You are an Antfarm workflow agent. Check for pending work and execute it.
Step 1 — Check for pending work:
\`\`\`
node ${cli} step claim "${fullAgentId}"
\`\`\`
If output is "NO_WORK", reply HEARTBEAT_OK and stop.
Step 2 — If JSON is returned, it contains: {"stepId": "...", "runId": "...", "input": "..."}
The "input" field contains your FULLY RESOLVED task instructions. All template variables have been replaced with actual values. Read the input carefully and DO the work it describes. This is the core of your job.
Step 3 — After completing the work, format your output with KEY: value lines (e.g., STATUS: done, REPO: /path, BRANCH: name, etc.) as specified in the task instructions.
Step 4 — Report completion. Write your full output to a temp file, then pipe it:
\`\`\`
cat <<'ANTFARM_EOF' > /tmp/antfarm-step-output.txt
STATUS: done
REPO: /path/to/repo
BRANCH: feature-branch
KEY: value
...
ANTFARM_EOF
cat /tmp/antfarm-step-output.txt | node ${cli} step complete "<stepId>"
\`\`\`
IMPORTANT: Always write output to a file first, then pipe via stdin. Do NOT pass output as a command-line argument — complex output (JSON, multi-line text) gets mangled by shell escaping.
This automatically: saves your output, merges KEY: value pairs into the run context, and advances the pipeline to the next step.
If the work FAILED and should be retried:
\`\`\`
node ${cli} step fail "<stepId>" "description of what went wrong"
\`\`\`
This handles retry logic automatically (retries up to max_retries, then fails the run).`;
}
export async function setupAgentCrons(workflow: WorkflowSpec): Promise<void> {
const agents = workflow.agents;
// Allow per-workflow cron interval via cron.interval_ms in workflow.yml
const everyMs = (workflow as any).cron?.interval_ms ?? DEFAULT_EVERY_MS;
for (let i = 0; i < agents.length; i++) {
const agent = agents[i];
const anchorMs = i * 60_000; // stagger by 1 minute each
const cronName = `antfarm/${workflow.id}/${agent.id}`;
const agentId = `${workflow.id}/${agent.id}`;
const prompt = buildAgentPrompt(workflow.id, agent.id);
const result = await createAgentCronJob({
name: cronName,
schedule: { kind: "every", everyMs, anchorMs },
sessionTarget: "isolated",
agentId,
payload: { kind: "agentTurn", message: prompt },
delivery: { mode: "none" },
enabled: true,
});
if (!result.ok) {
throw new Error(`Failed to create cron job for agent "${agent.id}": ${result.error}`);
}
}
}
export async function removeAgentCrons(workflowId: string): Promise<void> {
await deleteAgentCronJobs(`antfarm/${workflowId}/`);
}
// ── Run-scoped cron lifecycle ───────────────────────────────────────
/**
* Count active (running) runs for a given workflow.
*/
function countActiveRuns(workflowId: string): number {
const db = getDb();
const row = db.prepare(
"SELECT COUNT(*) as cnt FROM runs WHERE workflow_id = ? AND status = 'running'"
).get(workflowId) as { cnt: number };
return row.cnt;
}
/**
* Check if crons already exist for a workflow.
*/
async function workflowCronsExist(workflowId: string): Promise<boolean> {
const result = await listCronJobs();
if (!result.ok || !result.jobs) return false;
const prefix = `antfarm/${workflowId}/`;
return result.jobs.some((j) => j.name.startsWith(prefix));
}
/**
* Start crons for a workflow when a run begins.
* No-ops if crons already exist (another run of the same workflow is active).
*/
export async function ensureWorkflowCrons(workflow: WorkflowSpec): Promise<void> {
if (await workflowCronsExist(workflow.id)) return;
// Preflight: verify cron tool is accessible before attempting to create jobs
const preflight = await checkCronToolAvailable();
if (!preflight.ok) {
throw new Error(preflight.error!);
}
await setupAgentCrons(workflow);
}
/**
* Tear down crons for a workflow when a run ends.
* Only removes if no other active runs exist for this workflow.
*/
export async function teardownWorkflowCronsIfIdle(workflowId: string): Promise<void> {
const active = countActiveRuns(workflowId);
if (active > 0) return;
await removeAgentCrons(workflowId);
}