Reject incomplete autonomous cycles

This commit is contained in:
Theodore Song
2026-08-21 17:13:24 -04:00
parent 83e4ea8203
commit 6e6477a4e3
6 changed files with 22 additions and 16 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- run: node scripts/run-autonomous-cycle.mjs - run: node scripts/run-autonomous-cycle.mjs
env: env:
ARENA_URL: https://polymarket-site-eta.vercel.app ARENA_URL: https://polymarket-site-eta.vercel.app
EXPECTED_BUILD: "88" EXPECTED_BUILD: "89"
RUNTIME_BRANCH: runtime-state RUNTIME_BRANCH: runtime-state
RUNTIME_STATE_PATH: runtime/state.json RUNTIME_STATE_PATH: runtime/state.json
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -20,7 +20,7 @@ market snapshots. During an outage, cycles continue locally; cached entries are
allowed for 90 minutes, older snapshots become mark-only, and all cached data allowed for 90 minutes, older snapshots become mark-only, and all cached data
expires after 24 hours. expires after 24 hours.
Build 88 adds a headless GitHub Actions runtime that checks the production site Build 89 adds a headless GitHub Actions runtime that checks the production site
at seven minutes past every hour, continues from the previous agent snapshot, at seven minutes past every hour, continues from the previous agent snapshot,
and runs the next due paper cycle even when no browser is open. It writes a and runs the next due paper cycle even when no browser is open. It writes a
sanitized snapshot to the `runtime-state` branch and `/api/state` uses that as a sanitized snapshot to the `runtime-state` branch and `/api/state` uses that as a
+5 -5
View File
@@ -341,7 +341,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<nav class="topnav"> <nav class="topnav">
<div class="brand"> <div class="brand">
<div class="logo">🏆</div> <div class="logo">🏆</div>
<div><div class="brand-name">Polymarket Arena</div><div class="brand-sub">10 agents · 5 core + 5 aggressive</div><div class="build-badge">Adaptive strategy 59 · shock strategy 3 · autonomous runtime 1 · build 88</div></div> <div><div class="brand-name">Polymarket Arena</div><div class="brand-sub">10 agents · 5 core + 5 aggressive</div><div class="build-badge">Adaptive strategy 59 · shock strategy 3 · autonomous runtime 1 · build 89</div></div>
</div> </div>
<div class="tabs" id="tabs"> <div class="tabs" id="tabs">
<button class="tab" data-tab="overview">Overview</button> <button class="tab" data-tab="overview">Overview</button>
@@ -363,7 +363,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<div class="personal-banner" id="personalBanner"> <div class="personal-banner" id="personalBanner">
<b>Personal research mode.</b> This copy is for your own analysis, paper tracking, and manual trade research only. It does not pool money, onboard investors, custody funds, bypass eligibility rules, or place orders without your manual approval. <b>Personal research mode.</b> This copy is for your own analysis, paper tracking, and manual trade research only. It does not pool money, onboard investors, custody funds, bypass eligibility rules, or place orders without your manual approval.
</div> </div>
<div class="live-build-banner"><b>Build 88 active:</b> The public paper agents now have an hourly autonomous runner that continues when every user device is closed. A sanitized agent-only snapshot is written to a separate GitHub runtime branch and becomes the shared read-only fallback while Neon and Blob are unavailable. Personal accounts, emails, passwords, allocations, and live-money settings never enter that snapshot. The five aggressive agents retain Strategy 3's two-cent-stressed 12-hour shock fades, event deduplication, and forward demotion. Profits are not guaranteed.</div> <div class="live-build-banner"><b>Build 89 active:</b> The public paper agents now have an hourly autonomous runner that continues when every user device is closed. A sanitized agent-only snapshot is written to a separate GitHub runtime branch and becomes the shared read-only fallback while Neon and Blob are unavailable. Empty initialization snapshots are rejected, so a run cannot publish until it has analyzed live markets and produced agent decisions. Personal accounts, emails, passwords, allocations, and live-money settings never enter that snapshot. The five aggressive agents retain Strategy 3's two-cent-stressed 12-hour shock fades, event deduplication, and forward demotion. Profits are not guaranteed.</div>
<!-- ============ OVERVIEW ============ --> <!-- ============ OVERVIEW ============ -->
<section class="tabpanel" data-tab="overview"> <section class="tabpanel" data-tab="overview">
@@ -745,7 +745,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
</section> </section>
<footer> <footer>
Build 88 · Adaptive strategy 59 · Shock strategy 3 · Autonomous runtime 1 · Offline runtime 2 · Maker research 3 · Agent learning 3 · Paper trading only · Live prices from Polymarket's public Gamma and CLOB APIs · Not financial advice · Build 89 · Adaptive strategy 59 · Shock strategy 3 · Autonomous runtime 1 · Offline runtime 2 · Maker research 3 · Agent learning 3 · Paper trading only · Live prices from Polymarket's public Gamma and CLOB APIs · Not financial advice ·
<a class="market-link" href="https://github.com/theodore-song/polymarket-analyst" target="_blank" rel="noopener">Source on GitHub</a> <a class="market-link" href="https://github.com/theodore-song/polymarket-analyst" target="_blank" rel="noopener">Source on GitHub</a>
</footer> </footer>
</div> </div>
@@ -774,7 +774,7 @@ const POLITICS_TREND_MIN_HOLD_HOURS = 72;
const EXIT_CONFIRM_HOURS = 6; const EXIT_CONFIRM_HOURS = 6;
const AGENTS_KEY = "pma_agents_v2"; const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v5"; const SUG_KEY = "pma_suggestions_v5";
const BUILD_VERSION = 88; const BUILD_VERSION = 89;
const AGENT_LEARNING_VERSION = 3; const AGENT_LEARNING_VERSION = 3;
const SUGGESTION_ENGINE_VERSION = 59; const SUGGESTION_ENGINE_VERSION = 59;
const MAKER_STRATEGY_VERSION = 3; const MAKER_STRATEGY_VERSION = 3;
@@ -6785,7 +6785,7 @@ if("serviceWorker" in navigator){navigator.serviceWorker.register("/sw.js").catc
btn.disabled=true;btn.textContent="Starting…"; btn.disabled=true;btn.textContent="Starting…";
try{ try{
SNAP_TS=nowIso();AGENTS.forEach(a=>recordSnapshot(st.agents[a.id]));SNAP_TS=null; SNAP_TS=nowIso();AGENTS.forEach(a=>recordSnapshot(st.agents[a.id]));SNAP_TS=null;
st.seeded=true;st.date=todayStr();st.last_run=nowIso();st.last_cycle_hour=null;saveState(st); st.seeded=true;st.date=todayStr();st.last_run=null;st.last_cycle_hour=null;saveState(st);
await runDailyCycle();renderAll();toast("Live paper tracking started from a clean zero baseline."); await runDailyCycle();renderAll();toast("Live paper tracking started from a clean zero baseline.");
} }
catch(e){setStatus("start error — click Run",false);} catch(e){setStatus("start error — click Run",false);}
+9 -4
View File
@@ -24,7 +24,7 @@ function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
export function validateRuntimeSnapshot(snapshot, expectedBuild = 0) { export function validateRuntimeSnapshot(snapshot, expectedBuild = 0, { allowIncomplete = false } = {}) {
if (!snapshot || typeof snapshot !== "object" || !snapshot.items || typeof snapshot.items !== "object") { if (!snapshot || typeof snapshot !== "object" || !snapshot.items || typeof snapshot.items !== "object") {
throw new Error("Autonomous export is not a state snapshot"); throw new Error("Autonomous export is not a state snapshot");
} }
@@ -47,7 +47,12 @@ export function validateRuntimeSnapshot(snapshot, expectedBuild = 0) {
if (expectedBuild && Number(snapshot.build_version) !== Number(expectedBuild)) { if (expectedBuild && Number(snapshot.build_version) !== Number(expectedBuild)) {
throw new Error(`Expected Build ${expectedBuild}, received Build ${snapshot.build_version}`); throw new Error(`Expected Build ${expectedBuild}, received Build ${snapshot.build_version}`);
} }
if ((suggestions.suggestions || []).length > 300) throw new Error("Runtime suggestion snapshot exceeds the 300-item public limit"); 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)); 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}`); if (bytes > MAX_STATE_BYTES) throw new Error(`Runtime snapshot is ${bytes} bytes; limit is ${MAX_STATE_BYTES}`);
return { snapshot, bytes, agents, suggestions }; return { snapshot, bytes, agents, suggestions };
@@ -123,10 +128,10 @@ async function main() {
const branch = process.env.RUNTIME_BRANCH || "runtime-state"; const branch = process.env.RUNTIME_BRANCH || "runtime-state";
const pathname = process.env.RUNTIME_STATE_PATH || "runtime/state.json"; const pathname = process.env.RUNTIME_STATE_PATH || "runtime/state.json";
const arenaUrl = (process.env.ARENA_URL || "https://polymarket-site-eta.vercel.app").replace(/\/$/, ""); const arenaUrl = (process.env.ARENA_URL || "https://polymarket-site-eta.vercel.app").replace(/\/$/, "");
const expectedBuild = Number(required("EXPECTED_BUILD", "88")); const expectedBuild = Number(required("EXPECTED_BUILD", "89"));
await ensureRuntimeBranch(repository, branch); await ensureRuntimeBranch(repository, branch);
const prior = await readRuntimeFile(repository, branch, pathname); const prior = await readRuntimeFile(repository, branch, pathname);
if (prior.snapshot) validateRuntimeSnapshot(prior.snapshot); if (prior.snapshot) validateRuntimeSnapshot(prior.snapshot, 0, { allowIncomplete: true });
const browser = await chromium.launch({ headless: true }); const browser = await chromium.launch({ headless: true });
try { try {
+5 -4
View File
@@ -8,7 +8,7 @@ const workflow = fs.readFileSync(new URL("../.github/workflows/autonomous-cycle.
const runner = fs.readFileSync(new URL("./run-autonomous-cycle.mjs", import.meta.url), "utf8"); const runner = fs.readFileSync(new URL("./run-autonomous-cycle.mjs", import.meta.url), "utf8");
const build = Number(index.match(/const BUILD_VERSION = (\d+);/)?.[1]); const build = Number(index.match(/const BUILD_VERSION = (\d+);/)?.[1]);
assert.equal(build, 88); assert.equal(build, 89);
assert.deepEqual([...ALLOWED_RUNTIME_KEYS].sort(), ["pma_agents_v2", "pma_suggestions_v5"]); assert.deepEqual([...ALLOWED_RUNTIME_KEYS].sort(), ["pma_agents_v2", "pma_suggestions_v5"]);
assert.match(index, /function collectPublicRuntimeItems\(\)/); assert.match(index, /function collectPublicRuntimeItems\(\)/);
assert.match(index, /const PUBLIC_RUNTIME_KEYS=Object\.freeze\(\[AGENTS_KEY,SUG_KEY\]\)/); assert.match(index, /const PUBLIC_RUNTIME_KEYS=Object\.freeze\(\[AGENTS_KEY,SUG_KEY\]\)/);
@@ -19,11 +19,11 @@ assert.match(api, /runtime-state\/runtime\/state\.json/);
assert.match(api, /sanitizeGithubRuntimeState/); assert.match(api, /sanitizeGithubRuntimeState/);
assert.match(workflow, /cron: "7 \* \* \* \*"/); assert.match(workflow, /cron: "7 \* \* \* \*"/);
assert.match(workflow, /contents: write/); assert.match(workflow, /contents: write/);
assert.match(workflow, /EXPECTED_BUILD: "88"/); assert.match(workflow, /EXPECTED_BUILD: "89"/);
assert.match(runner, /MAX_STATE_BYTES = 900_000/); assert.match(runner, /MAX_STATE_BYTES = 900_000/);
const agentIds = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"]; const agentIds = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"];
const agents = Object.fromEntries(agentIds.map(id => [id, { cash: 10000, positions: [] }])); const agents = Object.fromEntries(agentIds.map(id => [id, { cash: 10000, positions: [], lastDecision: { mode: "test" } }]));
const snapshot = { const snapshot = {
schema_version: 1, schema_version: 1,
build_version: build, build_version: build,
@@ -31,11 +31,12 @@ const snapshot = {
last_cycle_hour: "2026-08-21T20|v59", last_cycle_hour: "2026-08-21T20|v59",
items: { items: {
pma_agents_v2: JSON.stringify({ seeded: true, last_cycle_hour: "2026-08-21T20|v59", agents }), pma_agents_v2: JSON.stringify({ seeded: true, last_cycle_hour: "2026-08-21T20|v59", agents }),
pma_suggestions_v5: JSON.stringify({ suggestions: [] }), pma_suggestions_v5: JSON.stringify({ suggestions: [{ market_id: "test" }] }),
}, },
}; };
assert.equal(validateRuntimeSnapshot(snapshot, build).agents.seeded, true); assert.equal(validateRuntimeSnapshot(snapshot, build).agents.seeded, true);
assert.throws(() => validateRuntimeSnapshot({ ...snapshot, items: { ...snapshot.items, pma_paper_accounts_v1: "{}" } }, build), /unexpected keys/); assert.throws(() => validateRuntimeSnapshot({ ...snapshot, items: { ...snapshot.items, pma_paper_accounts_v1: "{}" } }, build), /unexpected keys/);
assert.throws(() => validateRuntimeSnapshot({ ...snapshot, build_version: build - 1 }, build), /Expected Build/); assert.throws(() => validateRuntimeSnapshot({ ...snapshot, build_version: build - 1 }, build), /Expected Build/);
assert.throws(() => validateRuntimeSnapshot({ ...snapshot, items: { ...snapshot.items, pma_suggestions_v5: JSON.stringify({ suggestions: [] }) } }, build), /no live suggestions/);
console.log(`autonomous runtime verified for Build ${build}`); console.log(`autonomous runtime verified for Build ${build}`);
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = "polymarket-arena-build-88"; const CACHE_NAME = "polymarket-arena-build-89";
const APP_SHELL = ["/", "/index.html", "/personal.html", "/cycle-worker.js"]; const APP_SHELL = ["/", "/index.html", "/personal.html", "/cycle-worker.js"];
self.addEventListener("install", event => { self.addEventListener("install", event => {