mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-24 13:08:10 +00:00
Deploy hourly autonomous paper runtime
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
name: Autonomous paper cycle
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "7 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- index.html
|
||||||
|
- api/state.js
|
||||||
|
- scripts/run-autonomous-cycle.mjs
|
||||||
|
- .github/workflows/autonomous-cycle.yml
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: autonomous-paper-cycle
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cycle:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npx playwright install --with-deps chromium
|
||||||
|
- run: node scripts/run-autonomous-cycle.mjs
|
||||||
|
env:
|
||||||
|
ARENA_URL: https://polymarket-site-eta.vercel.app
|
||||||
|
EXPECTED_BUILD: "88"
|
||||||
|
RUNTIME_BRANCH: runtime-state
|
||||||
|
RUNTIME_STATE_PATH: runtime/state.json
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -20,6 +20,18 @@ 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
|
||||||
|
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
|
||||||
|
sanitized snapshot to the `runtime-state` branch and `/api/state` uses that as a
|
||||||
|
read-only fallback while Neon or Vercel Blob is unavailable. The public snapshot
|
||||||
|
contains only `pma_agents_v2` and `pma_suggestions_v5`, with suggestions capped at
|
||||||
|
300 to keep cross-device loads small. Paper accounts, passwords, email settings,
|
||||||
|
investment allocations, chat history, wallet information, and live-money
|
||||||
|
settings are explicitly excluded. Manual cycles can still run locally, but the
|
||||||
|
next autonomous hourly result is the shared public authority until managed
|
||||||
|
storage is restored.
|
||||||
|
|
||||||
Build 73 distinguishes a temporary order-book pause from settlement. Exact
|
Build 73 distinguishes a temporary order-book pause from settlement. Exact
|
||||||
market refreshes still mark paused positions to the latest published price, but
|
market refreshes still mark paused positions to the latest published price, but
|
||||||
the engine cannot simulate a stop, policy exit, or settlement while
|
the engine cannot simulate a stop, policy exit, or settlement while
|
||||||
|
|||||||
+76
-6
@@ -4,6 +4,8 @@ import { databaseConnectionDiagnostics, hasDatabase, readSharedAppState, writeSh
|
|||||||
const STATE_PATH = process.env.PMA_STATE_PATH || "shared/state.json";
|
const STATE_PATH = process.env.PMA_STATE_PATH || "shared/state.json";
|
||||||
const STATE_VERSION_PREFIX = process.env.PMA_STATE_VERSION_PREFIX || "shared/state-versions/";
|
const STATE_VERSION_PREFIX = process.env.PMA_STATE_VERSION_PREFIX || "shared/state-versions/";
|
||||||
const DATABASE_STATE_KEY = process.env.PMA_DATABASE_STATE_KEY || "polymarket-arena";
|
const DATABASE_STATE_KEY = process.env.PMA_DATABASE_STATE_KEY || "polymarket-arena";
|
||||||
|
const GITHUB_RUNTIME_STATE_URL = process.env.PMA_GITHUB_RUNTIME_STATE_URL
|
||||||
|
|| "https://raw.githubusercontent.com/theodore-song/polymarket-analyst/runtime-state/runtime/state.json";
|
||||||
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 PAPER_KEY = "pma_paper_accounts_v1";
|
const PAPER_KEY = "pma_paper_accounts_v1";
|
||||||
@@ -11,6 +13,7 @@ const LIVE_KEY = "pma_live_readiness_v1";
|
|||||||
const AGENT_IDS = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"];
|
const AGENT_IDS = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"];
|
||||||
const LIMITS = { closed: 80, history: 160, snapshots: 240, suggestions: 900, paperHistory: 120, paperSnapshots: 120, audit: 120 };
|
const LIMITS = { closed: 80, history: 160, snapshots: 240, suggestions: 900, paperHistory: 120, paperSnapshots: 120, audit: 120 };
|
||||||
const SIGNAL_LEDGER_LIMITS = { pending: 300, outcomes: 500 };
|
const SIGNAL_LEDGER_LIMITS = { pending: 300, outcomes: 500 };
|
||||||
|
const RUNTIME_ALLOWED_KEYS = new Set([AGENTS_KEY, SUG_KEY]);
|
||||||
|
|
||||||
function withBlobAuth(options = {}) {
|
function withBlobAuth(options = {}) {
|
||||||
const token = process.env.BLOB_READ_WRITE_TOKEN;
|
const token = process.env.BLOB_READ_WRITE_TOKEN;
|
||||||
@@ -80,19 +83,48 @@ async function readJsonBlob() {
|
|||||||
if (!primaryError) primaryError = err;
|
if (!primaryError) primaryError = err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (primaryError && !databaseAvailable) {
|
let githubError = null;
|
||||||
|
try {
|
||||||
|
const runtime = await readGithubRuntimeState();
|
||||||
|
if (runtime) return runtime;
|
||||||
|
} catch (err) {
|
||||||
|
githubError = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((primaryError || githubError) && !databaseAvailable) {
|
||||||
const error = new Error("Shared state providers are unavailable");
|
const error = new Error("Shared state providers are unavailable");
|
||||||
error.providers = {
|
error.providers = {
|
||||||
database: hasDatabase()
|
database: hasDatabase()
|
||||||
? { ...providerErrorMetadata(databaseError), ...databaseConnectionDiagnostics() }
|
? { ...providerErrorMetadata(databaseError), ...databaseConnectionDiagnostics() }
|
||||||
: { status: "not_configured", candidates: 0, urls: [] },
|
: { status: "not_configured", candidates: 0, urls: [] },
|
||||||
blob: providerErrorMetadata(primaryError),
|
blob: providerErrorMetadata(primaryError),
|
||||||
|
github_runtime: providerErrorMetadata(githubError),
|
||||||
};
|
};
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readGithubRuntimeState() {
|
||||||
|
if (!GITHUB_RUNTIME_STATE_URL) return null;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||||
|
try {
|
||||||
|
const url = new URL(GITHUB_RUNTIME_STATE_URL);
|
||||||
|
url.searchParams.set("minute", String(Math.floor(Date.now() / 60000)));
|
||||||
|
const response = await fetch(url, {
|
||||||
|
cache: "no-store",
|
||||||
|
headers: { Accept: "application/json", "Cache-Control": "no-cache" },
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (response.status === 404) return null;
|
||||||
|
if (!response.ok) throw new Error(`GitHub runtime state returned HTTP ${response.status}`);
|
||||||
|
return sanitizeGithubRuntimeState(await response.json());
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function persistState(state) {
|
async function persistState(state) {
|
||||||
let databaseSaved = false;
|
let databaseSaved = false;
|
||||||
let databaseError = null;
|
let databaseError = null;
|
||||||
@@ -275,6 +307,33 @@ function compactItems(items) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeGithubRuntimeState(payload) {
|
||||||
|
if (!payload || typeof payload !== "object" || !payload.items || typeof payload.items !== "object") {
|
||||||
|
throw new Error("Invalid GitHub runtime state");
|
||||||
|
}
|
||||||
|
const items = {};
|
||||||
|
for (const key of RUNTIME_ALLOWED_KEYS) {
|
||||||
|
if (typeof payload.items[key] === "string") items[key] = payload.items[key];
|
||||||
|
}
|
||||||
|
const agents = agentStateFromItems(items);
|
||||||
|
if (!agents || !agents.agents) throw new Error("GitHub runtime state is missing agent portfolios");
|
||||||
|
const suggestions = suggestionStateFromItems(items);
|
||||||
|
items[AGENTS_KEY] = JSON.stringify(compactAgentState(agents));
|
||||||
|
if (suggestions) items[SUG_KEY] = JSON.stringify(compactSuggestions(suggestions));
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
schema_version: Number(payload.schema_version || 1),
|
||||||
|
build_version: Number(payload.build_version || agents.engine_version || 0),
|
||||||
|
strategy_version: Number(payload.strategy_version || agents.strategy_version || 0),
|
||||||
|
generated_at: payload.generated_at || payload.updated_at || null,
|
||||||
|
updated_at: payload.updated_at || payload.generated_at || null,
|
||||||
|
last_cycle_hour: payload.last_cycle_hour || agents.last_cycle_hour || null,
|
||||||
|
source: "github-actions",
|
||||||
|
read_only: true,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function conflictResponse(res, error, current) {
|
function conflictResponse(res, error, current) {
|
||||||
return res.status(409).json({ ok: false, error, state: current });
|
return res.status(409).json({ ok: false, error, state: current });
|
||||||
}
|
}
|
||||||
@@ -282,15 +341,17 @@ function conflictResponse(res, error, current) {
|
|||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
res.setHeader("Cache-Control", "no-store");
|
res.setHeader("Cache-Control", "no-store");
|
||||||
try {
|
try {
|
||||||
if (!hasDatabase() && !process.env.BLOB_READ_WRITE_TOKEN && !process.env.VERCEL_OIDC_TOKEN) {
|
|
||||||
return res.status(503).json({ ok: false, error: "Cloud state is not configured" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
try {
|
try {
|
||||||
const state = await readJsonBlob();
|
const state = await readJsonBlob();
|
||||||
if (state && state.items) state.items = compactItems(state.items);
|
if (state && state.items) state.items = compactItems(state.items);
|
||||||
return res.status(200).json({ ok: true, state, degraded: false });
|
return res.status(200).json({
|
||||||
|
ok: true,
|
||||||
|
state,
|
||||||
|
degraded: false,
|
||||||
|
read_only: Boolean(state && state.read_only),
|
||||||
|
source: state && state.source || "managed-storage",
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// A storage outage must not prevent the installed app from using its local paper state.
|
// A storage outage must not prevent the installed app from using its local paper state.
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -304,6 +365,15 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
|
if (!hasDatabase() && !process.env.BLOB_READ_WRITE_TOKEN && !process.env.VERCEL_OIDC_TOKEN) {
|
||||||
|
return res.status(503).json({
|
||||||
|
ok: false,
|
||||||
|
degraded: true,
|
||||||
|
read_only: true,
|
||||||
|
source: "github-actions",
|
||||||
|
error: "Managed cloud state is not configured; autonomous shared state is read-only",
|
||||||
|
});
|
||||||
|
}
|
||||||
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
||||||
if (!body || typeof body !== "object" || !body.items || typeof body.items !== "object") {
|
if (!body || typeof body !== "object" || !body.items || typeof body.items !== "object") {
|
||||||
return res.status(400).json({ ok: false, error: "Invalid state payload" });
|
return res.status(400).json({ ok: false, error: "Invalid state payload" });
|
||||||
|
|||||||
+55
-17
@@ -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 · offline runtime 2 · build 87</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 88</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 87 active:</b> The five aggressive agents can adopt a two-cent-stressed shock strategy: fade an accelerating 8%+ three-hour move and exit after 12 hours at an executable price. The exact rule retained positive event-clustered confidence bounds in train, validation, chronological holdout, and sealed event holdout across 417 partition-events. Entries begin at 1% of paper equity, are distributed across agents without repeating an event, and can promote or demote from each agent's forward results. Offline installs now replace stale cached builds correctly. Profits are not guaranteed.</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>
|
||||||
|
|
||||||
<!-- ============ 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 87 · Adaptive strategy 59 · Shock strategy 3 · 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 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 ·
|
||||||
<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 = 87;
|
const BUILD_VERSION = 88;
|
||||||
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;
|
||||||
@@ -804,7 +804,9 @@ const PERSONAL_MODE = new URLSearchParams(location.search).get("personal") === "
|
|||||||
const PERSONAL_USER_ID = "local-readiness-user";
|
const PERSONAL_USER_ID = "local-readiness-user";
|
||||||
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
|
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
|
||||||
const SYNC_KEYS=[AGENTS_KEY,SUG_KEY,FOCUS_KEY,VIEW_KEY,PF_SORT_KEY,CHART_RANGE_KEY,INVEST_KEY,PAPER_KEY,LIVE_KEY,EMAIL_ALERT_KEY];
|
const SYNC_KEYS=[AGENTS_KEY,SUG_KEY,FOCUS_KEY,VIEW_KEY,PF_SORT_KEY,CHART_RANGE_KEY,INVEST_KEY,PAPER_KEY,LIVE_KEY,EMAIL_ALERT_KEY];
|
||||||
|
const PUBLIC_RUNTIME_KEYS=Object.freeze([AGENTS_KEY,SUG_KEY]);
|
||||||
const SYNC_LIMITS={closed:80,history:160,snapshots:240,suggestions:900,paperHistory:120,paperSnapshots:120,audit:120};
|
const SYNC_LIMITS={closed:80,history:160,snapshots:240,suggestions:900,paperHistory:120,paperSnapshots:120,audit:120};
|
||||||
|
const PUBLIC_RUNTIME_LIMITS=Object.freeze({...SYNC_LIMITS,suggestions:300});
|
||||||
const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff",
|
const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff",
|
||||||
"Pop Culture":"#c77dff",Other:"#94a1bb",All:"#7c8cff"};
|
"Pop Culture":"#c77dff",Other:"#94a1bb",All:"#7c8cff"};
|
||||||
const CLOB = "https://clob.polymarket.com";
|
const CLOB = "https://clob.polymarket.com";
|
||||||
@@ -1767,6 +1769,27 @@ function prepareCycleSuggestions(suggestions,runMode,entriesAllowed){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function collectSyncItems(){const items={};SYNC_KEYS.forEach(k=>{const v=localStorage.getItem(k);if(v!=null)items[k]=v;});return compactSyncItems(items);}
|
function collectSyncItems(){const items={};SYNC_KEYS.forEach(k=>{const v=localStorage.getItem(k);if(v!=null)items[k]=v;});return compactSyncItems(items);}
|
||||||
|
function collectPublicRuntimeItems(){
|
||||||
|
const st=compactAgentStateForSync(loadState(),PUBLIC_RUNTIME_LIMITS);
|
||||||
|
st.pending_sync=false;
|
||||||
|
st.runtime_managed=true;
|
||||||
|
const suggestions=compactSuggestionsForSync(loadSuggestions(),PUBLIC_RUNTIME_LIMITS);
|
||||||
|
return {[AGENTS_KEY]:JSON.stringify(st),[SUG_KEY]:JSON.stringify(suggestions)};
|
||||||
|
}
|
||||||
|
function buildPublicRuntimeSnapshot(){
|
||||||
|
const st=loadState();
|
||||||
|
return {schema_version:1,build_version:BUILD_VERSION,strategy_version:SUGGESTION_ENGINE_VERSION,
|
||||||
|
generated_at:nowIso(),updated_at:nowIso(),last_cycle_hour:st.last_cycle_hour||null,
|
||||||
|
source:"github-actions",read_only:true,items:collectPublicRuntimeItems(),
|
||||||
|
summary:{agents:AGENTS.map(cfg=>{const p=st.agents[cfg.id],eq=equity(p);return {id:cfg.id,equity:+eq.toFixed(2),
|
||||||
|
return_pct:+((eq/STARTING_BALANCE-1)*100).toFixed(3),cash:+Number(p.cash||0).toFixed(2),positions:(p.positions||[]).length};})}};
|
||||||
|
}
|
||||||
|
function applyPublicRuntimeSnapshot(snapshot){
|
||||||
|
if(!snapshot||typeof snapshot!=="object"||!snapshot.items||typeof snapshot.items!=="object")return false;
|
||||||
|
const items={};PUBLIC_RUNTIME_KEYS.forEach(key=>{if(typeof snapshot.items[key]==="string")items[key]=snapshot.items[key];});
|
||||||
|
if(!stateFromSyncItems(items))return false;
|
||||||
|
return applySyncItems(items);
|
||||||
|
}
|
||||||
function applySyncItems(items){
|
function applySyncItems(items){
|
||||||
if(!items||typeof items!=="object")return false;
|
if(!items||typeof items!=="object")return false;
|
||||||
const compact=compactSyncItems(items);
|
const compact=compactSyncItems(items);
|
||||||
@@ -1781,7 +1804,7 @@ function stateFromSyncItems(items){
|
|||||||
try{return items&&items[AGENTS_KEY]?JSON.parse(items[AGENTS_KEY]):null;}
|
try{return items&&items[AGENTS_KEY]?JSON.parse(items[AGENTS_KEY]):null;}
|
||||||
catch(e){return null;}
|
catch(e){return null;}
|
||||||
}
|
}
|
||||||
let CLOUD_STATE_HEALTH={ok:null,degraded:false,providers:null,error:null};
|
let CLOUD_STATE_HEALTH={ok:null,degraded:false,read_only:false,source:null,providers:null,error:null};
|
||||||
window.PMA_CLOUD_STATE_HEALTH=CLOUD_STATE_HEALTH;
|
window.PMA_CLOUD_STATE_HEALTH=CLOUD_STATE_HEALTH;
|
||||||
function rememberCloudHealth(next){
|
function rememberCloudHealth(next){
|
||||||
CLOUD_STATE_HEALTH={...CLOUD_STATE_HEALTH,...next};
|
CLOUD_STATE_HEALTH={...CLOUD_STATE_HEALTH,...next};
|
||||||
@@ -1789,6 +1812,7 @@ function rememberCloudHealth(next){
|
|||||||
return CLOUD_STATE_HEALTH;
|
return CLOUD_STATE_HEALTH;
|
||||||
}
|
}
|
||||||
function localOnlyStatus(base,providerOverride=null){
|
function localOnlyStatus(base,providerOverride=null){
|
||||||
|
if(CLOUD_STATE_HEALTH.read_only&&CLOUD_STATE_HEALTH.source==="github-actions")return `${base} · autonomous shared state`;
|
||||||
const providers=providerOverride||CLOUD_STATE_HEALTH.providers||{};
|
const providers=providerOverride||CLOUD_STATE_HEALTH.providers||{};
|
||||||
const dbStatus=providers.database&&providers.database.status;
|
const dbStatus=providers.database&&providers.database.status;
|
||||||
const blobStatus=providers.blob&&providers.blob.status;
|
const blobStatus=providers.blob&&providers.blob.status;
|
||||||
@@ -1798,6 +1822,7 @@ function localOnlyStatus(base,providerOverride=null){
|
|||||||
}
|
}
|
||||||
window.PMA_CLOUD_STATUS_LABEL=localOnlyStatus;
|
window.PMA_CLOUD_STATUS_LABEL=localOnlyStatus;
|
||||||
async function pushCloudState(force=false){
|
async function pushCloudState(force=false){
|
||||||
|
if(CLOUD_STATE_HEALTH.read_only)return false;
|
||||||
try{
|
try{
|
||||||
const r=await fetchWithTimeout("/api/state",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({items:collectSyncItems(),force})},NETWORK_REQUEST_TIMEOUT_MS);
|
const r=await fetchWithTimeout("/api/state",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({items:collectSyncItems(),force})},NETWORK_REQUEST_TIMEOUT_MS);
|
||||||
if(r.status===409){
|
if(r.status===409){
|
||||||
@@ -1810,10 +1835,10 @@ async function pushCloudState(force=false){
|
|||||||
}
|
}
|
||||||
const d=await r.json().catch(()=>null);
|
const d=await r.json().catch(()=>null);
|
||||||
if(!r.ok){
|
if(!r.ok){
|
||||||
rememberCloudHealth({ok:false,degraded:true,providers:d&&d.providers||null,error:d&&d.error||"Cloud sync unavailable"});
|
rememberCloudHealth({ok:false,degraded:true,read_only:Boolean(d&&d.read_only),source:d&&d.source||null,providers:d&&d.providers||null,error:d&&d.error||"Cloud sync unavailable"});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
rememberCloudHealth({ok:true,degraded:false,providers:null,error:null});
|
rememberCloudHealth({ok:true,degraded:false,read_only:false,source:"managed-storage",providers:null,error:null});
|
||||||
return true;
|
return true;
|
||||||
}catch(e){rememberCloudHealth({ok:false,degraded:true,error:"Cloud sync unavailable"});return false;}
|
}catch(e){rememberCloudHealth({ok:false,degraded:true,error:"Cloud sync unavailable"});return false;}
|
||||||
}
|
}
|
||||||
@@ -1822,11 +1847,11 @@ async function pullCloudState(){
|
|||||||
const r=await fetchWithTimeout("/api/state",{cache:"no-store"},NETWORK_REQUEST_TIMEOUT_MS);
|
const r=await fetchWithTimeout("/api/state",{cache:"no-store"},NETWORK_REQUEST_TIMEOUT_MS);
|
||||||
if(!r.ok){
|
if(!r.ok){
|
||||||
const d=await r.json().catch(()=>null);
|
const d=await r.json().catch(()=>null);
|
||||||
rememberCloudHealth({ok:false,degraded:true,providers:d&&d.providers||null,error:d&&d.error||"Cloud sync unavailable"});
|
rememberCloudHealth({ok:false,degraded:true,read_only:Boolean(d&&d.read_only),source:d&&d.source||null,providers:d&&d.providers||null,error:d&&d.error||"Cloud sync unavailable"});
|
||||||
return {ok:false,state:null,degraded:true,providers:d&&d.providers||null,error:d&&d.error};
|
return {ok:false,state:null,degraded:true,read_only:Boolean(d&&d.read_only),source:d&&d.source||null,providers:d&&d.providers||null,error:d&&d.error};
|
||||||
}
|
}
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
const result={ok:true,state:(d&&d.state)||null,degraded:Boolean(d&&d.degraded),error:d&&d.error,providers:d&&d.providers||null};
|
const result={ok:true,state:(d&&d.state)||null,degraded:Boolean(d&&d.degraded),read_only:Boolean(d&&d.read_only),source:d&&d.source||null,error:d&&d.error,providers:d&&d.providers||null};
|
||||||
rememberCloudHealth(result);
|
rememberCloudHealth(result);
|
||||||
return result;
|
return result;
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
@@ -1835,20 +1860,20 @@ async function pullCloudState(){
|
|||||||
}
|
}
|
||||||
async function loadAuthoritativeCloudState(){
|
async function loadAuthoritativeCloudState(){
|
||||||
const cloud=await pullCloudState();
|
const cloud=await pullCloudState();
|
||||||
if(!cloud.ok)return {ok:false,loaded:false,degraded:true};
|
if(!cloud.ok)return {ok:false,loaded:false,degraded:true,read_only:Boolean(cloud.read_only),source:cloud.source||null};
|
||||||
const cloudState=cloud.state;
|
const cloudState=cloud.state;
|
||||||
if(cloudState&&cloudState.items&&stateFromSyncItems(cloudState.items)){
|
if(cloudState&&cloudState.items&&stateFromSyncItems(cloudState.items)){
|
||||||
applySyncItems(cloudState.items);
|
applySyncItems(cloudState.items);
|
||||||
return {ok:true,loaded:true,degraded:Boolean(cloud.degraded),providers:cloud.providers||null};
|
return {ok:true,loaded:true,degraded:Boolean(cloud.degraded),read_only:Boolean(cloud.read_only),source:cloud.source||null,providers:cloud.providers||null};
|
||||||
}
|
}
|
||||||
return {ok:true,loaded:false,degraded:Boolean(cloud.degraded),providers:cloud.providers||null};
|
return {ok:true,loaded:false,degraded:Boolean(cloud.degraded),read_only:Boolean(cloud.read_only),source:cloud.source||null,providers:cloud.providers||null};
|
||||||
}
|
}
|
||||||
async function refreshFromCloudAndRender(silent=true){
|
async function refreshFromCloudAndRender(silent=true){
|
||||||
const cloud=await loadAuthoritativeCloudState();
|
const cloud=await loadAuthoritativeCloudState();
|
||||||
if(cloud.loaded){
|
if(cloud.loaded){
|
||||||
renderAll();
|
renderAll();
|
||||||
setStatus("cloud synced",false);
|
setStatus(cloud.read_only?"autonomous shared state loaded":"cloud synced",false);
|
||||||
if(!silent)toast("Shared cloud state refreshed.");
|
if(!silent)toast(cloud.read_only?"Autonomous agent state refreshed.":"Shared cloud state refreshed.");
|
||||||
}else if(cloud.degraded){
|
}else if(cloud.degraded){
|
||||||
setStatus(localOnlyStatus("cloud storage paused"),false);
|
setStatus(localOnlyStatus("cloud storage paused"),false);
|
||||||
if(!silent)toast("Cloud storage is unavailable. This device is using its local backup.");
|
if(!silent)toast("Cloud storage is unavailable. This device is using its local backup.");
|
||||||
@@ -5631,6 +5656,8 @@ window.PMA_ENGINE_DIAGNOSTICS=Object.freeze({
|
|||||||
applyShockFadeForwardPromotion,
|
applyShockFadeForwardPromotion,
|
||||||
stageShockFadeShadows,
|
stageShockFadeShadows,
|
||||||
manageShockFadeShadows,
|
manageShockFadeShadows,
|
||||||
|
autonomousRuntime:Object.freeze({version:1,publicKeys:[...PUBLIC_RUNTIME_KEYS],suggestionLimit:PUBLIC_RUNTIME_LIMITS.suggestions,
|
||||||
|
excludedKeys:[PAPER_KEY,EMAIL_ALERT_KEY,INVEST_KEY,LIVE_KEY,CHAT_KEY,PAID_AGENT_CHAT_KEY]}),
|
||||||
marketIsSettled,
|
marketIsSettled,
|
||||||
marketIsSuspended,
|
marketIsSuspended,
|
||||||
historicalPriceFeatures,
|
historicalPriceFeatures,
|
||||||
@@ -6268,7 +6295,11 @@ function runEngineSelfTest(){
|
|||||||
marketLearning:{samples:152,current_samples:127,pending:300,buckets:{}}}});
|
marketLearning:{samples:152,current_samples:127,pending:300,buckets:{}}}});
|
||||||
const freshCountReport=decisionSummary({lastDecision:{mode:"Test",reason:"test",maxNew:1,maxFrac:0.03,
|
const freshCountReport=decisionSummary({lastDecision:{mode:"Test",reason:"test",maxNew:1,maxFrac:0.03,
|
||||||
marketLearning:calibrationDecisionRecord(calibrationProfile)}});
|
marketLearning:calibrationDecisionRecord(calibrationProfile)}});
|
||||||
|
const runtimeSnapshot=buildPublicRuntimeSnapshot(),runtimeKeys=Object.keys(runtimeSnapshot.items).sort();
|
||||||
return {buildVersion:BUILD_VERSION,version:SUGGESTION_ENGINE_VERSION,
|
return {buildVersion:BUILD_VERSION,version:SUGGESTION_ENGINE_VERSION,
|
||||||
|
autonomousRuntime:{exactPublicKeys:runtimeKeys.join(",")===[AGENTS_KEY,SUG_KEY].sort().join(","),
|
||||||
|
excludesPrivateKeys:[PAPER_KEY,EMAIL_ALERT_KEY,INVEST_KEY,LIVE_KEY,CHAT_KEY,PAID_AGENT_CHAT_KEY].every(key=>!runtimeKeys.includes(key)),
|
||||||
|
suggestionLimit:PUBLIC_RUNTIME_LIMITS.suggestions},
|
||||||
trend:{ready:trend.trade_ready,quality:trend.quality,side:trend.side,margin:trend.net_edge},
|
trend:{ready:trend.trade_ready,quality:trend.quality,side:trend.side,margin:trend.net_edge},
|
||||||
auditedTrendCategories:{sportsObservationOnly:!sportsTrend.trade_ready&&sportsTrend.signal_type==="trend",cryptoObservationOnly:!cryptoTrend.trade_ready&&cryptoTrend.signal_type==="trend"},
|
auditedTrendCategories:{sportsObservationOnly:!sportsTrend.trade_ready&&sportsTrend.signal_type==="trend",cryptoObservationOnly:!cryptoTrend.trade_ready&&cryptoTrend.signal_type==="trend"},
|
||||||
noSignal:{ready:noSignal.trade_ready,quality:noSignal.quality,signal:noSignal.signal_type,margin:noSignal.net_edge},
|
noSignal:{ready:noSignal.trade_ready,quality:noSignal.quality,signal:noSignal.signal_type,margin:noSignal.net_edge},
|
||||||
@@ -6571,6 +6602,13 @@ function runEngineSelfTest(){
|
|||||||
chatIdentityMigrates:chatMigrationFixture.reversal[0].text.includes("Shock Reversion")&&!chatMigrationFixture.reversal[0].text.includes("Reversal Edge")&&!chatMigrationFixture.reversal[0].text.includes("Trend Endurance"),
|
chatIdentityMigrates:chatMigrationFixture.reversal[0].text.includes("Shock Reversion")&&!chatMigrationFixture.reversal[0].text.includes("Reversal Edge")&&!chatMigrationFixture.reversal[0].text.includes("Trend Endurance"),
|
||||||
immaterialRunnerDoesNotBlock,rejectionAccounting,convictionCapacityCoversTarget,rules:window.PMA_ENGINE_DIAGNOSTICS.rules};
|
immaterialRunnerDoesNotBlock,rejectionAccounting,convictionCapacityCoversTarget,rules:window.PMA_ENGINE_DIAGNOSTICS.rules};
|
||||||
}
|
}
|
||||||
|
window.PMA_AUTOMATION=Object.freeze({
|
||||||
|
status:()=>{const st=loadState();return {build:BUILD_VERSION,strategy:SUGGESTION_ENGINE_VERSION,running:CYCLE_RUNNING,
|
||||||
|
seeded:Boolean(st.seeded),last_cycle_hour:st.last_cycle_hour||null,suggestion_count:(loadSuggestions().suggestions||[]).length};},
|
||||||
|
runCycle:async()=>{const result=await runDailyCycle();renderAll();return result;},
|
||||||
|
exportShared:()=>buildPublicRuntimeSnapshot(),
|
||||||
|
importShared:(snapshot)=>{const applied=applyPublicRuntimeSnapshot(snapshot);if(applied)renderAll();return applied;},
|
||||||
|
});
|
||||||
if(new URLSearchParams(location.search).get("engine_test")==="1"){
|
if(new URLSearchParams(location.search).get("engine_test")==="1"){
|
||||||
const output=document.createElement("output");output.id="engineSelfTest";output.hidden=true;output.textContent=JSON.stringify(runEngineSelfTest());document.body.appendChild(output);
|
const output=document.createElement("output");output.id="engineSelfTest";output.hidden=true;output.textContent=JSON.stringify(runEngineSelfTest());document.body.appendChild(output);
|
||||||
}
|
}
|
||||||
@@ -6735,8 +6773,8 @@ if("serviceWorker" in navigator){navigator.serviceWorker.register("/sw.js").catc
|
|||||||
const cloud=await loadAuthoritativeCloudState();
|
const cloud=await loadAuthoritativeCloudState();
|
||||||
let st=loadState();
|
let st=loadState();
|
||||||
if(cloud.loaded){
|
if(cloud.loaded){
|
||||||
toast("Cloud state loaded for this device.");
|
toast(cloud.read_only?"Autonomous agent state loaded for this device.":"Cloud state loaded for this device.");
|
||||||
}else if(cloud.ok&&!cloud.degraded&&st.seeded){
|
}else if(cloud.ok&&!cloud.degraded&&!cloud.read_only&&st.seeded){
|
||||||
pushCloudState();
|
pushCloudState();
|
||||||
}else if(cloud.degraded||!cloud.ok){
|
}else if(cloud.degraded||!cloud.ok){
|
||||||
setStatus(localOnlyStatus("offline-ready"),false);
|
setStatus(localOnlyStatus("offline-ready"),false);
|
||||||
|
|||||||
Generated
+54
-3
@@ -4,11 +4,15 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "polymarket-analyst",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neondatabase/serverless": "^1.1.0",
|
"@neondatabase/serverless": "^1.1.0",
|
||||||
"@vercel/blob": "2.5.0",
|
"@vercel/blob": "2.5.0",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
"svix": "^1.96.1"
|
"svix": "^1.96.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"playwright": "^1.62.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@neondatabase/serverless": {
|
"node_modules/@neondatabase/serverless": {
|
||||||
@@ -131,6 +135,21 @@
|
|||||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||||
"license": "Unlicense"
|
"license": "Unlicense"
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-stream": {
|
"node_modules/get-stream": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||||
@@ -277,6 +296,38 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.62.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/retry": {
|
"node_modules/retry": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||||
@@ -354,9 +405,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.27.0",
|
"version": "6.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
|
||||||
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
|
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
|
|||||||
+5
-1
@@ -14,12 +14,16 @@
|
|||||||
"evaluate:sports-favorites": "node scripts/evaluate-sports-favorites.mjs",
|
"evaluate:sports-favorites": "node scripts/evaluate-sports-favorites.mjs",
|
||||||
"test:server-state": "node scripts/test-server-state.mjs",
|
"test:server-state": "node scripts/test-server-state.mjs",
|
||||||
"test:shock-audit": "node scripts/test-shock-audit.mjs",
|
"test:shock-audit": "node scripts/test-shock-audit.mjs",
|
||||||
"test:offline-runtime": "node scripts/test-offline-runtime.mjs"
|
"test:offline-runtime": "node scripts/test-offline-runtime.mjs",
|
||||||
|
"test:autonomous-runtime": "node scripts/test-autonomous-runtime.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neondatabase/serverless": "^1.1.0",
|
"@neondatabase/serverless": "^1.1.0",
|
||||||
"@vercel/blob": "2.5.0",
|
"@vercel/blob": "2.5.0",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
"svix": "^1.96.1"
|
"svix": "^1.96.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"playwright": "^1.62.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
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) {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
if ((suggestions.suggestions || []).length > 300) throw new Error("Runtime suggestion snapshot exceeds the 300-item public limit");
|
||||||
|
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", "88"));
|
||||||
|
await ensureRuntimeBranch(repository, branch);
|
||||||
|
const prior = await readRuntimeFile(repository, branch, pathname);
|
||||||
|
if (prior.snapshot) validateRuntimeSnapshot(prior.snapshot);
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { ALLOWED_RUNTIME_KEYS, validateRuntimeSnapshot } from "./run-autonomous-cycle.mjs";
|
||||||
|
|
||||||
|
const index = fs.readFileSync(new URL("../index.html", import.meta.url), "utf8");
|
||||||
|
const api = fs.readFileSync(new URL("../api/state.js", import.meta.url), "utf8");
|
||||||
|
const workflow = fs.readFileSync(new URL("../.github/workflows/autonomous-cycle.yml", 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]);
|
||||||
|
|
||||||
|
assert.equal(build, 88);
|
||||||
|
assert.deepEqual([...ALLOWED_RUNTIME_KEYS].sort(), ["pma_agents_v2", "pma_suggestions_v5"]);
|
||||||
|
assert.match(index, /function collectPublicRuntimeItems\(\)/);
|
||||||
|
assert.match(index, /const PUBLIC_RUNTIME_KEYS=Object\.freeze\(\[AGENTS_KEY,SUG_KEY\]\)/);
|
||||||
|
assert.match(index, /suggestions:300/);
|
||||||
|
assert.match(index, /window\.PMA_AUTOMATION=Object\.freeze/);
|
||||||
|
assert.match(index, /if\(CLOUD_STATE_HEALTH\.read_only\)return false/);
|
||||||
|
assert.match(api, /runtime-state\/runtime\/state\.json/);
|
||||||
|
assert.match(api, /sanitizeGithubRuntimeState/);
|
||||||
|
assert.match(workflow, /cron: "7 \* \* \* \*"/);
|
||||||
|
assert.match(workflow, /contents: write/);
|
||||||
|
assert.match(workflow, /EXPECTED_BUILD: "88"/);
|
||||||
|
assert.match(runner, /MAX_STATE_BYTES = 900_000/);
|
||||||
|
|
||||||
|
const agentIds = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"];
|
||||||
|
const agents = Object.fromEntries(agentIds.map(id => [id, { cash: 10000, positions: [] }]));
|
||||||
|
const snapshot = {
|
||||||
|
schema_version: 1,
|
||||||
|
build_version: build,
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
last_cycle_hour: "2026-08-21T20|v59",
|
||||||
|
items: {
|
||||||
|
pma_agents_v2: JSON.stringify({ seeded: true, last_cycle_hour: "2026-08-21T20|v59", agents }),
|
||||||
|
pma_suggestions_v5: JSON.stringify({ suggestions: [] }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
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, build_version: build - 1 }, build), /Expected Build/);
|
||||||
|
|
||||||
|
console.log(`autonomous runtime verified for Build ${build}`);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { databaseConnectionDiagnostics, databaseUrl, databaseUrls, normalizeDatabaseUrl } from "../lib/db.js";
|
import { databaseConnectionDiagnostics, databaseUrl, databaseUrls, normalizeDatabaseUrl } from "../lib/db.js";
|
||||||
import { compactAgentState, compactSuggestion, providerErrorCode } from "../api/state.js";
|
import { compactAgentState, compactSuggestion, providerErrorCode, sanitizeGithubRuntimeState } from "../api/state.js";
|
||||||
|
|
||||||
assert.equal(normalizeDatabaseUrl("psql 'postgresql://user:pass@example.test/db?sslmode=require'"),
|
assert.equal(normalizeDatabaseUrl("psql 'postgresql://user:pass@example.test/db?sslmode=require'"),
|
||||||
"postgresql://user:pass@example.test/db?sslmode=require");
|
"postgresql://user:pass@example.test/db?sslmode=require");
|
||||||
@@ -63,6 +63,27 @@ const compactedState = compactAgentState({
|
|||||||
assert.equal(compactedState.signal_ledger.expired_ungraded, 7);
|
assert.equal(compactedState.signal_ledger.expired_ungraded, 7);
|
||||||
assert.equal(compactedState.agents.reversal.shock_fade_shadows[0].shock_strategy_version, 3);
|
assert.equal(compactedState.agents.reversal.shock_fade_shadows[0].shock_strategy_version, 3);
|
||||||
assert.equal(compactedState.agents.reversal.shock_fade_outcomes[0].net_return, 0.08);
|
assert.equal(compactedState.agents.reversal.shock_fade_outcomes[0].net_return, 0.08);
|
||||||
|
|
||||||
|
const publicAgents = Object.fromEntries([
|
||||||
|
"value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction",
|
||||||
|
].map(id => [id, { cash: 10000, positions: [], closed: [], history: [], snapshots: [] }]));
|
||||||
|
const sanitizedRuntime = sanitizeGithubRuntimeState({
|
||||||
|
schema_version: 1,
|
||||||
|
build_version: 88,
|
||||||
|
generated_at: "2026-08-21T20:00:00.000Z",
|
||||||
|
items: {
|
||||||
|
pma_agents_v2: JSON.stringify({ seeded: true, last_cycle_hour: "2026-08-21T20|v59", agents: publicAgents }),
|
||||||
|
pma_suggestions_v5: JSON.stringify({ suggestions: [] }),
|
||||||
|
pma_paper_accounts_v1: JSON.stringify({ password: "must-not-survive" }),
|
||||||
|
pma_trade_email_alerts_v1: JSON.stringify({ email: "private@example.com" }),
|
||||||
|
pma_live_readiness_v1: JSON.stringify({ wallet: "private" }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(Object.keys(sanitizedRuntime.items).sort(), ["pma_agents_v2", "pma_suggestions_v5"]);
|
||||||
|
assert.equal(sanitizedRuntime.read_only, true);
|
||||||
|
assert.equal(sanitizedRuntime.source, "github-actions");
|
||||||
|
assert.equal(sanitizedRuntime.build_version, 88);
|
||||||
|
assert.throws(() => sanitizeGithubRuntimeState({ items: { pma_suggestions_v5: "{}" } }), /missing agent portfolios/);
|
||||||
assert.equal(providerErrorCode(new Error("403 Forbidden")), "authorization_failed");
|
assert.equal(providerErrorCode(new Error("403 Forbidden")), "authorization_failed");
|
||||||
assert.equal(providerErrorCode(new Error("Error connecting to database: HTTP status 402")), "provider_payment_required");
|
assert.equal(providerErrorCode(new Error("Error connecting to database: HTTP status 402")), "provider_payment_required");
|
||||||
assert.equal(providerErrorCode(new Error("invalid connection string")), "invalid_connection_string");
|
assert.equal(providerErrorCode(new Error("invalid connection string")), "invalid_connection_string");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = "polymarket-arena-build-87";
|
const CACHE_NAME = "polymarket-arena-build-88";
|
||||||
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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user