Files
polymarket-analyst_github/scripts/run-autonomous-cycle.mjs
T

182 lines
8.2 KiB
JavaScript

import { chromium } from "playwright";
import { pathToFileURL } from "node:url";
export const AGENTS_KEY = "pma_agents_v2";
export const SUGGESTIONS_KEY = "pma_suggestions_v5";
export const ALLOWED_RUNTIME_KEYS = Object.freeze([AGENTS_KEY, SUGGESTIONS_KEY]);
const FORBIDDEN_RUNTIME_KEYS = Object.freeze([
"pma_paper_accounts_v1",
"pma_trade_email_alerts_v1",
"pma_invest_allocations_v1",
"pma_live_readiness_v1",
"pma_agent_chat_v1",
"pma_paid_agent_chat_v1",
]);
const MAX_STATE_BYTES = 900_000;
function required(name, fallback = "") {
const value = process.env[name] || fallback;
if (!value) throw new Error(`${name} is required`);
return value;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function validateRuntimeSnapshot(snapshot, expectedBuild = 0, { allowIncomplete = false } = {}) {
if (!snapshot || typeof snapshot !== "object" || !snapshot.items || typeof snapshot.items !== "object") {
throw new Error("Autonomous export is not a state snapshot");
}
const keys = Object.keys(snapshot.items).sort();
const allowed = [...ALLOWED_RUNTIME_KEYS].sort();
if (JSON.stringify(keys) !== JSON.stringify(allowed)) {
throw new Error(`Autonomous export contains unexpected keys: ${keys.join(", ")}`);
}
for (const key of FORBIDDEN_RUNTIME_KEYS) {
if (Object.prototype.hasOwnProperty.call(snapshot.items, key)) throw new Error(`Private key ${key} cannot enter autonomous state`);
}
for (const key of ALLOWED_RUNTIME_KEYS) {
if (typeof snapshot.items[key] !== "string") throw new Error(`Runtime item ${key} must be serialized JSON`);
JSON.parse(snapshot.items[key]);
}
const agents = JSON.parse(snapshot.items[AGENTS_KEY]);
const suggestions = JSON.parse(snapshot.items[SUGGESTIONS_KEY]);
if (!agents.agents || Object.keys(agents.agents).length !== 10) throw new Error("Runtime snapshot must contain ten public agents");
if (!agents.seeded || !agents.last_cycle_hour) throw new Error("Runtime snapshot has not completed a cycle");
if (expectedBuild && Number(snapshot.build_version) !== Number(expectedBuild)) {
throw new Error(`Expected Build ${expectedBuild}, received Build ${snapshot.build_version}`);
}
const suggestionCount = (suggestions.suggestions || []).length;
if (suggestionCount > 300) throw new Error("Runtime suggestion snapshot exceeds the 300-item public limit");
if (!allowIncomplete && suggestionCount === 0) throw new Error("Runtime cycle produced no live suggestions");
if (!allowIncomplete && Object.values(agents.agents).some(agent => !agent?.lastDecision)) {
throw new Error("Runtime cycle did not produce a decision for every agent");
}
const bytes = Buffer.byteLength(JSON.stringify(snapshot));
if (bytes > MAX_STATE_BYTES) throw new Error(`Runtime snapshot is ${bytes} bytes; limit is ${MAX_STATE_BYTES}`);
return { snapshot, bytes, agents, suggestions };
}
async function githubRequest(path, options = {}) {
const token = required("GITHUB_TOKEN");
const response = await fetch(`https://api.github.com${path}`, {
...options,
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "polymarket-arena-autonomous-runtime",
...(options.headers || {}),
},
});
if (response.status === 404 && options.allowNotFound) return null;
const body = await response.json().catch(() => null);
if (!response.ok) throw new Error(`GitHub API ${options.method || "GET"} ${path} failed with HTTP ${response.status}: ${body?.message || "unknown error"}`);
return body;
}
async function ensureRuntimeBranch(repository, branch) {
const encoded = encodeURIComponent(`heads/${branch}`);
const current = await githubRequest(`/repos/${repository}/git/ref/${encoded}`, { allowNotFound: true });
if (current) return;
await githubRequest(`/repos/${repository}/git/refs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: required("GITHUB_SHA") }),
});
}
async function readRuntimeFile(repository, branch, pathname) {
const file = await githubRequest(`/repos/${repository}/contents/${pathname}?ref=${encodeURIComponent(branch)}`, { allowNotFound: true });
if (!file || file.type !== "file" || !file.content) return { snapshot: null, sha: null };
try {
return { snapshot: JSON.parse(Buffer.from(file.content.replace(/\s/g, ""), "base64").toString("utf8")), sha: file.sha };
} catch {
throw new Error("Existing runtime state is not valid JSON");
}
}
async function writeRuntimeFile(repository, branch, pathname, snapshot, sha) {
const body = {
message: `Update autonomous paper cycle ${snapshot.last_cycle_hour}`,
content: Buffer.from(`${JSON.stringify(snapshot, null, 2)}\n`).toString("base64"),
branch,
};
if (sha) body.sha = sha;
await githubRequest(`/repos/${repository}/contents/${pathname}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
async function waitForProductionBuild(page, arenaUrl, expectedBuild) {
const deadline = Date.now() + 12 * 60_000;
while (Date.now() < deadline) {
await page.goto(`${arenaUrl}?automation=1&build=${expectedBuild}&t=${Date.now()}`, { waitUntil: "domcontentloaded", timeout: 90_000 });
await page.waitForFunction(() => Boolean(window.PMA_AUTOMATION), null, { timeout: 20_000 }).catch(() => {});
const status = await page.evaluate(() => window.PMA_AUTOMATION?.status?.() || null);
if (Number(status?.build) === expectedBuild) return status;
await sleep(20_000);
}
throw new Error(`Production did not reach Build ${expectedBuild} before the autonomous cycle deadline`);
}
async function main() {
const repository = required("GITHUB_REPOSITORY", "theodore-song/polymarket-analyst");
const branch = process.env.RUNTIME_BRANCH || "runtime-state";
const pathname = process.env.RUNTIME_STATE_PATH || "runtime/state.json";
const arenaUrl = (process.env.ARENA_URL || "https://polymarket-site-eta.vercel.app").replace(/\/$/, "");
const expectedBuild = Number(required("EXPECTED_BUILD", "102"));
await ensureRuntimeBranch(repository, branch);
const prior = await readRuntimeFile(repository, branch, pathname);
if (prior.snapshot) validateRuntimeSnapshot(prior.snapshot, 0, { allowIncomplete: true });
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({ serviceWorkers: "block" });
if (prior.snapshot) {
await context.addInitScript(({ snapshot, allowedKeys }) => {
try {
for (const key of allowedKeys) {
if (typeof snapshot.items?.[key] === "string") localStorage.setItem(key, snapshot.items[key]);
}
} catch {}
}, { snapshot: prior.snapshot, allowedKeys: ALLOWED_RUNTIME_KEYS });
}
const page = await context.newPage();
const pageErrors = [];
page.on("pageerror", error => pageErrors.push(String(error?.message || error)));
await waitForProductionBuild(page, arenaUrl, expectedBuild);
await page.waitForFunction(() => {
const status = window.PMA_AUTOMATION?.status?.();
return Boolean(status?.seeded && !status?.running);
}, null, { timeout: 12 * 60_000 });
await page.evaluate(() => window.PMA_AUTOMATION.runCycle());
await page.waitForFunction(() => !window.PMA_AUTOMATION?.status?.().running, null, { timeout: 12 * 60_000 });
const snapshot = await page.evaluate(() => window.PMA_AUTOMATION.exportShared());
const validated = validateRuntimeSnapshot(snapshot, expectedBuild);
await writeRuntimeFile(repository, branch, pathname, snapshot, prior.sha);
const status = await page.evaluate(() => window.PMA_AUTOMATION.status());
console.log(JSON.stringify({
ok: true,
build: status.build,
cycle: snapshot.last_cycle_hour,
suggestions: validated.suggestions.suggestions?.length || 0,
bytes: validated.bytes,
agent_summary: snapshot.summary?.agents || [],
page_errors: pageErrors.slice(0, 3),
}));
} finally {
await browser.close();
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(error => {
console.error(error?.stack || error);
process.exitCode = 1;
});
}