From c5f59aa9921439223c1c4cd649ef2b32d5468173 Mon Sep 17 00:00:00 2001 From: Theodore Song Date: Sat, 22 Aug 2026 21:01:15 -0400 Subject: [PATCH] Keep autonomous cycles alive under state growth --- .github/workflows/autonomous-cycle.yml | 4 +- scripts/run-autonomous-cycle.mjs | 96 ++++++++++++++++++++++---- scripts/test-autonomous-runtime.mjs | 38 +++++++++- 3 files changed, 122 insertions(+), 16 deletions(-) diff --git a/.github/workflows/autonomous-cycle.yml b/.github/workflows/autonomous-cycle.yml index 7b7760f..1b80721 100644 --- a/.github/workflows/autonomous-cycle.yml +++ b/.github/workflows/autonomous-cycle.yml @@ -40,7 +40,7 @@ jobs: RUNTIME_STATE_PATH: runtime/state.json GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Wait for the next five-minute slot - if: success() + if: always() shell: bash run: | now=$(date +%s) @@ -49,7 +49,7 @@ jobs: echo "Next autonomous slot starts in ${delay}s" sleep "$delay" - name: Dispatch the next autonomous cycle - if: success() + if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/scripts/run-autonomous-cycle.mjs b/scripts/run-autonomous-cycle.mjs index c362f00..9f669ef 100644 --- a/scripts/run-autonomous-cycle.mjs +++ b/scripts/run-autonomous-cycle.mjs @@ -16,10 +16,12 @@ const MAX_STATE_BYTES = 900_000; const TRANSPORT_HISTORY_LIMIT = 24; const TRANSPORT_SNAPSHOT_LIMIT = 96; const TRANSPORT_MAKER_OUTCOME_LIMIT = 30; -const TRANSPORT_TARGET_BYTES = 875_000; +const TRANSPORT_TARGET_BYTES = 850_000; const TRANSPORT_HISTORY_FLOOR = 12; const TRANSPORT_MAKER_OUTCOME_FLOOR = 12; const TRANSPORT_SUGGESTION_FLOOR = 240; +const TRANSPORT_SIGNAL_OUTCOME_RESERVE = 144; +const TRANSPORT_SNAPSHOT_FALLBACKS = Object.freeze([72, 48, 24]); function sampleSnapshots(rows, limit = TRANSPORT_SNAPSHOT_LIMIT) { const list = Array.isArray(rows) ? rows : []; @@ -113,24 +115,92 @@ export function compactRuntimeTransportSnapshot(snapshot) { portfolio.lastDecision = compactDecision(portfolio.lastDecision); } suggestions.suggestions = (suggestions.suggestions || []).slice(0, 300).map(compactPublicSuggestion); - out.items[AGENTS_KEY] = JSON.stringify(state); - out.items[SUGGESTIONS_KEY] = JSON.stringify(suggestions); - if (Buffer.byteLength(JSON.stringify(out)) > TRANSPORT_TARGET_BYTES) { + const writeItems = () => { + out.items[AGENTS_KEY] = JSON.stringify(state); + out.items[SUGGESTIONS_KEY] = JSON.stringify(suggestions); + return Buffer.byteLength(JSON.stringify(out)); + }; + let bytes = writeItems(); + if (bytes > TRANSPORT_TARGET_BYTES) { for (const portfolio of Object.values(state.agents || {})) { portfolio.history = (portfolio.history || []).slice(-TRANSPORT_HISTORY_FLOOR); portfolio.maker_outcomes = (portfolio.maker_outcomes || []).slice(-TRANSPORT_MAKER_OUTCOME_FLOOR); } suggestions.suggestions = suggestions.suggestions.slice(0, TRANSPORT_SUGGESTION_FLOOR); - out.items[AGENTS_KEY] = JSON.stringify(state); - out.items[SUGGESTIONS_KEY] = JSON.stringify(suggestions); + bytes = writeItems(); } - if (out.summary && out.summary.signal_ledger && state.signal_ledger) { - const pending = Array.isArray(state.signal_ledger.pending) ? state.signal_ledger.pending.length : 0; - const outcomes = Array.isArray(state.signal_ledger.outcomes) ? state.signal_ledger.outcomes.length : 0; - out.summary.signal_ledger.pending_retained = pending; - out.summary.signal_ledger.pending_total = Math.max(Number(out.summary.signal_ledger.pending_total || 0), pending); - out.summary.signal_ledger.outcomes_retained = outcomes; - out.summary.signal_ledger.outcomes_total = Math.max(Number(out.summary.signal_ledger.outcomes_total || 0), outcomes); + const ledger = state.signal_ledger && typeof state.signal_ledger === "object" ? state.signal_ledger : null; + const pending = Array.isArray(ledger?.pending) ? ledger.pending : []; + const outcomes = Array.isArray(ledger?.outcomes) ? ledger.outcomes : []; + if (bytes > TRANSPORT_TARGET_BYTES && ledger) { + ledger.pending = []; + ledger.outcomes = outcomes.slice(-Math.min(TRANSPORT_SIGNAL_OUTCOME_RESERVE, outcomes.length)); + bytes = writeItems(); + + for (const limit of TRANSPORT_SNAPSHOT_FALLBACKS) { + if (bytes <= TRANSPORT_TARGET_BYTES) break; + for (const portfolio of Object.values(state.agents || {})) { + portfolio.snapshots = sampleSnapshots(portfolio.snapshots, limit); + } + bytes = writeItems(); + } + + if (bytes > TRANSPORT_TARGET_BYTES && ledger.outcomes.length) { + let low = 0; + let high = ledger.outcomes.length; + while (low < high) { + const mid = Math.ceil((low + high) / 2); + ledger.outcomes = outcomes.slice(-mid); + if (writeItems() <= TRANSPORT_TARGET_BYTES) low = mid; + else high = mid - 1; + } + ledger.outcomes = low ? outcomes.slice(-low) : []; + bytes = writeItems(); + } + + let pendingLow = 0; + let pendingHigh = pending.length; + while (pendingLow < pendingHigh) { + const mid = Math.ceil((pendingLow + pendingHigh) / 2); + ledger.pending = pending.slice(0, mid); + if (writeItems() <= TRANSPORT_TARGET_BYTES) pendingLow = mid; + else pendingHigh = mid - 1; + } + ledger.pending = pending.slice(0, pendingLow); + bytes = writeItems(); + + if (pendingLow === pending.length && ledger.outcomes.length < outcomes.length) { + let outcomeLow = ledger.outcomes.length; + let outcomeHigh = outcomes.length; + while (outcomeLow < outcomeHigh) { + const mid = Math.ceil((outcomeLow + outcomeHigh) / 2); + ledger.outcomes = outcomes.slice(-mid); + if (writeItems() <= TRANSPORT_TARGET_BYTES) outcomeLow = mid; + else outcomeHigh = mid - 1; + } + ledger.outcomes = outcomeLow ? outcomes.slice(-outcomeLow) : []; + writeItems(); + } + } + const syncLedgerSummary = () => { + if (!out.summary?.signal_ledger || !ledger) return; + out.summary.signal_ledger.pending_retained = ledger.pending.length; + out.summary.signal_ledger.pending_total = Math.max(Number(out.summary.signal_ledger.pending_total || 0), pending.length); + out.summary.signal_ledger.outcomes_retained = ledger.outcomes.length; + out.summary.signal_ledger.outcomes_total = Math.max(Number(out.summary.signal_ledger.outcomes_total || 0), outcomes.length); + out.summary.signal_ledger.byte_budget = TRANSPORT_TARGET_BYTES; + }; + syncLedgerSummary(); + bytes = writeItems(); + while (bytes > TRANSPORT_TARGET_BYTES && ledger?.pending.length) { + ledger.pending.pop(); + syncLedgerSummary(); + bytes = writeItems(); + } + while (bytes > TRANSPORT_TARGET_BYTES && ledger?.outcomes.length) { + ledger.outcomes.shift(); + syncLedgerSummary(); + bytes = writeItems(); } return out; } diff --git a/scripts/test-autonomous-runtime.mjs b/scripts/test-autonomous-runtime.mjs index 88e9cce..918e0a5 100644 --- a/scripts/test-autonomous-runtime.mjs +++ b/scripts/test-autonomous-runtime.mjs @@ -73,6 +73,7 @@ assert.match(api, /searchParams\.set\("runtime", `\$\{Date\.now\(\)\}/); assert.match(workflow, /cron: "2,7,12,17,22,27,32,37,42,47,52,57 \* \* \* \*"/); assert.match(workflow, /contents: write/); assert.match(workflow, /EXPECTED_BUILD: "124"/); +assert.equal((workflow.match(/if: always\(\)/g) || []).length, 2); assert.match(routingReleaseWorkflow, /name: Release protected agent routing after Vercel quota reset/); assert.match(routingReleaseWorkflow, /routesExclusiveNoPairToTailAlpha/); assert.match(routingReleaseWorkflow, /cron: "25 1-23\/2 23 8 \*"/); @@ -177,7 +178,7 @@ assert.match(runner, /MAX_STATE_BYTES = 900_000/); assert.match(runner, /compactRuntimeTransportSnapshot\(rawSnapshot\)/); assert.match(runner, /localStorage\.getItem\(agentKey\)/); assert.match(runner, /localStorage\.getItem\(suggestionsKey\)/); -assert.match(runner, /TRANSPORT_TARGET_BYTES = 875_000/); +assert.match(runner, /TRANSPORT_TARGET_BYTES = 850_000/); assert.match(runner, /TRANSPORT_SUGGESTION_FLOOR = 240/); assert.match(runner, /Production already advanced to Build \$\{status\.build\}; retiring stale Build \$\{expectedBuild\} runner/); assert.equal(resolutionAudit.strategy, "resolution-window-no-50-55-forward-shadow-v3"); @@ -268,4 +269,39 @@ assert.equal(transportOutput.summary.signal_ledger.pending_retained, 50); assert.equal(transportOutput.summary.signal_ledger.outcomes_retained, 144); assert.ok(Buffer.byteLength(JSON.stringify(transportOutput)) < Buffer.byteLength(JSON.stringify(transportInput))); +const stressState = structuredClone(transportState); +stressState.signal_ledger = { + pending: Array.from({ length: 600 }, (_, index) => ({ + key: `stress-pending-${index}`, + market_id: `pending-market-${index}`, + observed_at: new Date(1_700_000_000_000 + index * 1_000).toISOString(), + side: "YES", + learning_payload: "p".repeat(1_200), + })), + outcomes: Array.from({ length: 1_000 }, (_, index) => ({ + key: `stress-outcome-${index}`, + market_id: `outcome-market-${index}`, + evaluated_at: new Date(1_700_000_000_000 + index * 1_000).toISOString(), + return: index / 10_000, + learning_payload: "o".repeat(1_200), + })), +}; +const stressInput = { + ...transportInput, + summary: { signal_ledger: { pending_retained: 600, pending_total: 600, outcomes_retained: 1_000, outcomes_total: 1_000 } }, + items: { ...transportInput.items, pma_agents_v2: JSON.stringify(stressState) }, +}; +const stressOutput = compactRuntimeTransportSnapshot(stressInput); +const stressedState = JSON.parse(stressOutput.items.pma_agents_v2); +assert.ok(Buffer.byteLength(JSON.stringify(stressOutput)) <= 850_000); +assert.equal(stressedState.agents.value.positions[0].market_id, "position-0"); +assert.ok(stressedState.signal_ledger.pending.length > 0); +assert.ok(stressedState.signal_ledger.outcomes.length > 0); +assert.equal(stressedState.signal_ledger.pending[0].key, "stress-pending-0"); +assert.equal(stressedState.signal_ledger.outcomes.at(-1).key, "stress-outcome-999"); +assert.equal(stressOutput.summary.signal_ledger.pending_retained, stressedState.signal_ledger.pending.length); +assert.equal(stressOutput.summary.signal_ledger.pending_total, 600); +assert.equal(stressOutput.summary.signal_ledger.outcomes_retained, stressedState.signal_ledger.outcomes.length); +assert.equal(stressOutput.summary.signal_ledger.outcomes_total, 1_000); + console.log(`autonomous runtime verified for Build ${build}`);