Harden database state failover diagnostics

This commit is contained in:
Theodore Song
2026-08-19 08:28:24 -04:00
parent 78b832f926
commit 45421c985a
6 changed files with 22 additions and 16 deletions
+5 -5
View File
@@ -15,12 +15,12 @@ https://polymarket-site-eta.vercel.app/personal.html
The site fetches live Polymarket markets, generates agent suggestions, lets you The site fetches live Polymarket markets, generates agent suggestions, lets you
run frequent paper cycles, and syncs the shared arena state through Neon or run frequent paper cycles, and syncs the shared arena state through Neon or
Vercel Blob. Build 58 also installs an offline app shell and caches timestamped Vercel Blob. Build 59 also installs an offline app shell and caches timestamped
market snapshots. During an outage, cycles continue locally; cached entries are 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 58 ranks the competition by each agent's return since Strategy 50 began. Build 59 ranks the competition by each agent's return since Strategy 50 began.
Historical replay equity remains visible for context, but it no longer makes an Historical replay equity remains visible for context, but it no longer makes an
agent look like the current leader when the live adaptive strategy is losing. agent look like the current leader when the live adaptive strategy is losing.
@@ -114,7 +114,7 @@ feature views before repeatable positive evidence can increase size or repeatabl
negative evidence can block a new entry. Mixed evidence stays close to neutral negative evidence can block a new entry. Mixed evidence stays close to neutral
instead of being mistaken for an edge. instead of being mistaken for an edge.
Build 58 enforces the documented offline boundary end to end. Cached snapshots Build 59 enforces the documented offline boundary end to end. Cached snapshots
under 90 minutes old may continue paper execution. Older snapshots remain usable under 90 minutes old may continue paper execution. Older snapshots remain usable
for valuation and chart snapshots for up to 24 hours, but cannot trigger entries, for valuation and chart snapshots for up to 24 hours, but cannot trigger entries,
stop-losses, gain-stops, risk rebalances, settlements, or policy exits. Network stop-losses, gain-stops, risk rebalances, settlements, or policy exits. Network
@@ -127,12 +127,12 @@ adaptive baselines, pending signal grades, and trade evidence remain in one stra
lineage until the actual entry, sizing, or exit logic changes. Legacy build 40 and 41 lineage until the actual entry, sizing, or exit logic changes. Legacy build 40 and 41
records are migrated into the same strategy lineage without losing evidence. records are migrated into the same strategy lineage without losing evidence.
Build 58 independently refreshes markets for matured pending signals that have Build 59 independently refreshes markets for matured pending signals that have
left the current top-500 activity scan. Unavailable markets remain queued for a left the current top-500 activity scan. Unavailable markets remain queued for a
bounded retry window. This prevents activity-rank survivorship from deciding bounded retry window. This prevents activity-rank survivorship from deciding
which wins and losses reach the adaptive calibration ledger. which wins and losses reach the adaptive calibration ledger.
Build 58 also allocates the 300 pending observation slots by evidence coverage. Build 59 also allocates the 300 pending observation slots by evidence coverage.
Under-sampled signal/side/category cohorts are observed first, followed by Under-sampled signal/side/category cohorts are observed first, followed by
under-sampled independent events and market sides, with conviction used only as under-sampled independent events and market sides, with conviction used only as
a later tie-breaker. This prevents the same popular contracts from monopolizing a later tie-breaker. This prevents the same popular contracts from monopolizing
+2 -3
View File
@@ -19,9 +19,8 @@ export function normalizeDatabaseUrl(raw) {
export function databaseUrls() { export function databaseUrls() {
const candidates = [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL] const candidates = [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL]
.map(normalizeDatabaseUrl).filter(Boolean); .map(normalizeDatabaseUrl).filter(Boolean);
const ordered = [...candidates.filter(value => /^postgres(?:ql)?:\/\//i.test(value)), const valid = candidates.filter(value => /^postgres(?:ql)?:\/\//i.test(value));
...candidates.filter(value => !/^postgres(?:ql)?:\/\//i.test(value))]; return [...new Set(valid.length ? valid : candidates)];
return [...new Set(ordered)];
} }
export function databaseUrl() { export function databaseUrl() {
+6
View File
@@ -19,7 +19,13 @@ function withBlobAuth(options = {}) {
export function providerErrorCode(error) { export function providerErrorCode(error) {
if (!error) return "not_configured_or_not_attempted"; if (!error) return "not_configured_or_not_attempted";
const code = String(error.code || error.cause && error.cause.code || "").toUpperCase();
const message = String(error.message || error).toLowerCase(); const message = String(error.message || error).toLowerCase();
if (code === "28P01" || code === "28000") return "authorization_failed";
if (code === "3D000") return "database_not_found";
if (code.startsWith("08")) return "connection_failed";
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "dns_failed";
if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") return "timeout";
if (message.includes("invalid") && (message.includes("url") || message.includes("connection string"))) return "invalid_connection_string"; if (message.includes("invalid") && (message.includes("url") || message.includes("connection string"))) return "invalid_connection_string";
if (message.includes("password authentication") || message.includes("unauthorized") || /\b(?:401|403)\b/.test(message)) return "authorization_failed"; if (message.includes("password authentication") || message.includes("unauthorized") || /\b(?:401|403)\b/.test(message)) return "authorization_failed";
if (message.includes("enotfound") || message.includes("getaddrinfo") || message.includes("dns")) return "dns_failed"; if (message.includes("enotfound") || message.includes("getaddrinfo") || message.includes("dns")) return "dns_failed";
+6 -6
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 50 · build 58</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 50 · build 59</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 58 active:</b> unproven directional signals train the event-clustered learner without risking cash. New observations prioritize under-sampled strategy cohorts and independent events before repeats. Cohorts can trade only after positive promotion. Live-priced complete YES or NO negative-risk bundles may trade when their worst-case payout remains positive after estimated costs; cached bundle prices never open positions. This remains paper trading; profits are not guaranteed.</div> <div class="live-build-banner"><b>Build 59 active:</b> unproven directional signals train the event-clustered learner without risking cash. New observations prioritize under-sampled strategy cohorts and independent events before repeats, while shared state normalizes and fails over across configured Neon URLs. Cohorts can trade only after positive promotion. Live-priced complete YES or NO negative-risk bundles may trade when their worst-case payout remains positive after estimated costs; cached bundle prices never open positions. This remains paper trading; 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 58 · Adaptive strategy 50 · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice · Build 59 · Adaptive strategy 50 · Paper trading only · Live prices from Polymarket's public Gamma API · 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 = 58; const BUILD_VERSION = 59;
const SUGGESTION_ENGINE_VERSION = 50; const SUGGESTION_ENGINE_VERSION = 50;
const PREVIOUS_STRATEGY_VERSION = 49; const PREVIOUS_STRATEGY_VERSION = 49;
const LEGACY_BUILD_STRATEGY_LINEAGE = Object.freeze({40:40,41:40}); const LEGACY_BUILD_STRATEGY_LINEAGE = Object.freeze({40:40,41:40});
@@ -4737,11 +4737,11 @@ function runEngineSelfTest(){
minConv:0,maxNew:1,maxFrac:0.04,reserve:0.10,targetExposure:0.60,learning:buildAdaptiveProfile(staleEntryBook),marketLearning:{samples:0,pending:0,buckets:{}}, minConv:0,maxNew:1,maxFrac:0.04,reserve:0.10,targetExposure:0.60,learning:buildAdaptiveProfile(staleEntryBook),marketLearning:{samples:0,pending:0,buckets:{}},
},new Set(),{}); },new Set(),{});
const buildMigrationState=defaultState(); const buildMigrationState=defaultState();
buildMigrationState.engine_version=57;buildMigrationState.strategy_version=SUGGESTION_ENGINE_VERSION; buildMigrationState.engine_version=58;buildMigrationState.strategy_version=SUGGESTION_ENGINE_VERSION;
buildMigrationState.agents.value.engine_baseline={version:SUGGESTION_ENGINE_VERSION,started_at:hoursAgo(2),equity:9876.54}; buildMigrationState.agents.value.engine_baseline={version:SUGGESTION_ENGINE_VERSION,started_at:hoursAgo(2),equity:9876.54};
reconcileStateVersions(buildMigrationState); reconcileStateVersions(buildMigrationState);
const strategyMigrationState=defaultState(); const strategyMigrationState=defaultState();
strategyMigrationState.engine_version=57;strategyMigrationState.strategy_version=PREVIOUS_STRATEGY_VERSION; strategyMigrationState.engine_version=58;strategyMigrationState.strategy_version=PREVIOUS_STRATEGY_VERSION;
strategyMigrationState.agents.value.cash=9876.54; strategyMigrationState.agents.value.cash=9876.54;
strategyMigrationState.agents.value.engine_baseline={version:PREVIOUS_STRATEGY_VERSION,started_at:hoursAgo(2),equity:10000}; strategyMigrationState.agents.value.engine_baseline={version:PREVIOUS_STRATEGY_VERSION,started_at:hoursAgo(2),equity:10000};
reconcileStateVersions(strategyMigrationState); reconcileStateVersions(strategyMigrationState);
+2 -1
View File
@@ -13,7 +13,7 @@ const originalNeonUrl = process.env.NEON_DATABASE_URL;
process.env.DATABASE_URL = "not-a-postgres-url"; process.env.DATABASE_URL = "not-a-postgres-url";
process.env.NEON_DATABASE_URL = "postgresql://alias:pass@example.test/db"; process.env.NEON_DATABASE_URL = "postgresql://alias:pass@example.test/db";
assert.equal(databaseUrl(), process.env.NEON_DATABASE_URL); assert.equal(databaseUrl(), process.env.NEON_DATABASE_URL);
assert.deepEqual(databaseUrls(), [process.env.NEON_DATABASE_URL, "not-a-postgres-url"]); assert.deepEqual(databaseUrls(), [process.env.NEON_DATABASE_URL]);
process.env.DATABASE_URL = "postgresql://stale:pass@example.test/db"; process.env.DATABASE_URL = "postgresql://stale:pass@example.test/db";
assert.deepEqual(databaseUrls(), [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL]); assert.deepEqual(databaseUrls(), [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL]);
if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL; if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL;
@@ -40,5 +40,6 @@ const compactedState = compactAgentState({
assert.equal(compactedState.signal_ledger.expired_ungraded, 7); assert.equal(compactedState.signal_ledger.expired_ungraded, 7);
assert.equal(providerErrorCode(new Error("403 Forbidden")), "authorization_failed"); assert.equal(providerErrorCode(new Error("403 Forbidden")), "authorization_failed");
assert.equal(providerErrorCode(new Error("invalid connection string")), "invalid_connection_string"); assert.equal(providerErrorCode(new Error("invalid connection string")), "invalid_connection_string");
assert.equal(providerErrorCode(Object.assign(new Error("database rejected login"), { code: "28P01" })), "authorization_failed");
console.log("server state tests passed"); console.log("server state tests passed");
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = "polymarket-arena-build-58"; const CACHE_NAME = "polymarket-arena-build-59";
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 => {