Repository navigation
Calculate budget windows on the detailed-save path, five years at a time - #1054
Merged
Merged
Conversation
A budget window's years are report runs naming its budget-window run. The tests cover the store that starts the earliest waiting years while at most five are under way, fails the window and its waiting years when a year fails or Modal refuses a year's call, marks the window succeeded when its last year succeeds, marks it running when a year is claimed, and settles a window that no year is advancing. They also cover the stuck-run sweep's listing, which leaves a window's waiting years to the window. They fail because the store does not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…outcome The Stage 12 persistence library gains a budget-window store. An advance locks the budget-window run's row, fails the window and its waiting years (and their unstarted simulation runs) when a year failed, marks it succeeded once every year succeeded, and otherwise starts the earliest waiting years until five are under way, recording each call while the year is still pending with no call. A start Modal refuses fails that year and the window. A claimed year marks its window running, and a window that no year is advancing can be settled: finished if its years have ended, failed as stalled if years are still waiting. The stuck-run sweep's listing leaves a year waiting in an unfinished window to the window, and names the window of each report run it lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…failing) When the stuck-run sweep fails a year of a budget window, it should fail the window too, starting nothing, and after judging the runs it should settle each window that no year has advanced for the minimum age. The scheduled function should pass the budget-window store to the sweep and return the windows it finished. They fail because the sweep takes no window store yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the sweep fails a year of a budget window, it fails the window too, with the window's waiting years, and starts nothing. After judging the runs, it settles each unfinished window that nothing has changed for the minimum age with no year under way: from its years when they have all ended, or as stalled when years are still waiting. The scheduled function gives the sweep the budget-window store, built from the same Modal worker database secret, and returns the windows it failed or marked succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ailing) A year's coordinator should mark its window running when it claims the year, and when the year ends advance the window, starting the next years' coordinators in its own Modal application by report run ID. Neither step may change the year's own outcome: a failure is logged without its text, and only a pre-emption or cancellation propagates. The store should name the window a report run is a year of. They fail because neither exists yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A coordinator marks its year's window running when it claims the year and, once the year has ended, advances the window: it starts the earliest waiting years in its own Modal application's coordinate_report by report run ID, or finishes the window. Each step logs a failure by its type only and never changes the year's outcome; a pre-emption or cancellation propagates. The store names the window a report run is a year of. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coordinator should hold three budget windows at five years each beside the ten calls it had for other reports, each simulation worker both simulations of every coordinator call, and the US segment function every segment of every US simulation call, so no call waits behind a container limit while its caller's wait runs. They fail on today's limits of 10, 10 and 300. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coordinator may run 25 calls at once: three budget windows at five years each, and the ten it had for other reports. Each simulation worker may run 50, both simulations of every coordinator call, and the US segment function 1,000, one per region group of every US simulation call, so no call waits behind a limit while its caller's wait runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rst (failing) A year differs from the window's first year only in its year, its three runs, its artifact prefix and when its call was made, so whoever starts a year derives its coordinator input from the first year's. The store should give its starter each year with its runs, and a coordinator's year starter should spawn its own application's coordinator with the derived input. They fail because the derivation and the year passed to the starter do not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract library derives a year's coordinator input and context from another year's: only the year, the three runs, the artifact prefix and the call's creation time change, and the retention period keeps its length. It also builds a run's artifact prefix, so the simulation service and the coordinator name it alike. The budget-window store gives its starter each waiting year with its runs, and a coordinator's year starter spawns its own application's coordinate_report with the derived input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ailing) The simulation service fails a budget-window run when Modal refuses one of the first years it starts, so its ready check should require UPDATE on the window run's status, error message, completion and update times, as the Public API's privilege manifest is to grant. It fails because the check lists no budget-window columns yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y check The simulation service fails a budget-window run when Modal refuses one of the first years it starts, so its ready check now requires UPDATE on the window run's status, error message, completion and update times. The Public API grants them in PolicyEngine/policyengine-api#4267, which must deploy before this does, or the service's ready check fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A year an earlier year's coordinator started late should count from when its call was recorded, not from its creation at submit, so the sweep's maximum age doesn't fail a year queued behind Modal's container limit. A start Modal refuses should log its cause's type and give that year's own simulation runs the refusal. The artifact prefix should refuse an environment that could leave it and a creation time without a time zone, as the executor's prefix does. A new test confirms the simulation service's role can start and fail a window with the same store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A year an earlier year's coordinator started counts, until its claim, from its last change, when its call was recorded, so the sweep's maximum age no longer fails a year that waits in Modal's queue a day after the service created it. A start Modal refuses is logged by the error's type, and that year's simulation runs record the refusal instead of another year's failure. The contract's artifact prefix refuses an environment that could leave it and a creation time without a time zone, and the executor's comparison prefix now uses it. The store's description says the window lock is held across the Modal calls that start years. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v2 coordinator function should mark its year's window running before the coordinator runs and advance the window after the coordinator returns, fails or is cancelled, but not after a pre-emption, which Modal reruns. A call naming no report run touches no window, and a window store that cannot be built leaves the year to run. The year starter should take the coordinator's raw input and check it only when it starts a year. They fail because the wrapper does not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v2 coordinator function marks its year's window running before the coordinator runs and advances the window once the coordinator returns, fails or is cancelled, starting the window's next years in its own application. A pre-emption advances nothing: Modal reruns the call, which advances when it ends, even when it finds its year already finished. A call naming no report run touches no window, and a window store that can't be built is logged by type and leaves the year to run. The year starter takes the coordinator's raw input and checks it only when it starts a year. This also rewraps the sweep's description, which review round 1's fix left with an overlong line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges v2plan/c1-8-run-table-coordination (#1051): the coordinator takes a report run's input and context with no evaluation record, and each worker claims its own simulation run. The Stage 12 persistence README and the Modal worker database access module keep both branches' descriptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1 found that the year starter's call didn't fit main's coordinator, which required an evaluation record. Merging C1 · 8 made the record optional for report runs, which fixed it; these tests keep it fixed. The started call binds to the v2 coordinator function's signature with no record, and the real coordinator claims the started year's report run under the started call and calculates it. They couldn't be written failing first: the run-table test helpers they use arrived with the merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A budget-window report reads its catalog rows and both sides' values once, and each year's reusable runs, and is created with one budget-window run and a report run per year. Each year has its own simulations, of that year, so reuse works between a window's years and single-year reports. A window must name consecutive years from its first, at most 75. They fail because the window records don't exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation persistence library creates a budget-window report as an economy report with its window's length, one pending budget-window run, and for each year a report run naming it and the year, on the same versions. Each year's sides that reuse no run get a new simulation and run of that year, so a window's years and single-year reports reuse each other's runs. The store reads the catalog rows and both sides' values once, and each year's reusable runs. A window's years must be 1 to 75 consecutive years from its first, ending by 2200. The economy records' helpers that insert a simulation with its run and turn a refused key into EconomyRecordReferenceError are now public for it, and a refused budget-window run key is named too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window failed as stalled should record the stall on its waiting years and their simulation runs, not another year's failure. A refused start should name its cause's type in the year's message. The coordinator's wrapper should look the year's window up once, make no window writes for a report run outside any window, look it up again when that first lookup failed, and still advance the window when the call's observability context can't be read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window failed as stalled records the stall on its waiting years and their simulation runs, not another year's failure, and a refused start names its cause's type in the year's message. The coordinator's wrapper looks the year's window up once: a report run outside any window makes no window writes, a failed lookup is retried once the year ends, and a call whose observability context can't be read still advances its window. The artifact environment check lives once, in the contract. The store and scheduling descriptions say the window is marked running before the coordinator claims its year, and the Stage 12 migration document describes budget windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation service waits a limited time for Modal to accept a call, and Modal may accept it later, its coordinator claiming the year before the failed start is recorded. That year is under way, so the window should go on starting its other years rather than fail. It fails because any failed start fails the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a start raises but the year is no longer waiting, because Modal accepted its call after the starter stopped waiting and the coordinator claimed it, the year counts as started and the advance goes on with the next years. Only a year still waiting is failed as refused, with its window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A budget-window request names its first year and its window's length (1 to 75 years, ending by 2200), sends no cliff option, and stores the options a single-year report without cliffs stores. The service reads the window's inputs once, creates a report run per year, then advances the window, which starts its first five years; the first year's coordinator input is built as a single-year report's, and each later year's is derived from it. A year reuses its own year's run. A first year Modal refuses or doesn't confirm in time answers 503 naming the report, and an advance that can't be written is answered as created. The route sends a window to the window service and an economy report to the report service. They fail because the window request, service and route don't exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /reports should take an economy report or a budget window, told apart by report_type, and answer 201 with either's created record. The budget-window request should name its length (1 to 75) and no cliff option, and its schemas should be the Cloud Run entrypoint's own. They fail because the route takes only economy reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /reports runs through a route class that relocates a refused body's errors as an economy report's were: the matched request type leaves each field's location, and a missing or unknown report_type is refused at report_type. The first years a request starts share one ten-second wait for Modal; a later year not confirmed within it is deferred (YearStartDeferred), which leaves it and the later years waiting for a started year's coordinator instead of failing the window. The window service's logs use the economy service's record fields, the window request shares the economy request's helpers and the last-year bound, an unused property is gone, and the route description, the changelog fragment and the README describe budget windows, including what GET /reports answers for one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unknown report_type should be refused with the value sent, whatever its JSON type, and a body with none as an economy report's was. The first years' shared wait for Modal should start when the first year starts, not before the advance's lock and reads. Two guards: a later year Modal refuses outright still fails the window, and no later year's call starts once the wait has run out. The value test and the wait test fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first years' shared wait for Modal starts with the first start, after the advance's lock and reads, so database time no longer comes out of the first year's wait. An unknown report_type is refused with the value sent, as JSON, not its text. The report types and the expected-types message are derived from the request models, and the window records use the tables' bounds instead of copies. A deferred later year is logged. The README, the window service's and store's descriptions, the route description and the Stage 12 migration document describe the shared wait and deferral, and that a body with no report_type is refused for that alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing) When the first year's call uses the whole shared wait, the next year is deferred without its call being started, and the deferral's log names the year's report run. It fails because the log names only the year. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deferred year's log names its report run as well as its year. The report route's body type is one shared alias, from which the error relocation reads the report types. The README and the window service's description say up to the first five years start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reports' into v2plan/c1-13-budget-windows
The simulation service's economy calls now name their artifact prefix with report_run_artifact_prefix, as each budget-window year derived from them does, so both build it alike and refuse an environment that could leave the prefix or a creation time without a time zone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 tasks done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reports' into v2plan/c1-13-budget-windows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from
v2plan/c1-9-economy-simulations-reports
to
main
November 12, 2026 20:23
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Public API's #4267 grants the simulation service's role UPDATE on budget_window_runs' status, error_message, completed_at and updated_at, which the service's readiness check and its window advances need. Its merge commit is the head of the Public API's master. Its v2 migrations and privilege manifest are the same as at the #4267 head the budget-window store and records tests ran against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation service's role now needs UPDATE on budget_window_runs' status, error_message, completed_at and updated_at, so the fixture of every needed privilege lists them, and the fixture missing two others still reports only those two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… passes on The v2 coordinate_report hands the runtime's capture_context to coordinate_window_year, which uses it to start a budget window's next years. The real runtime has it; the test's recording runtime didn't, so coordinate_report's cases failed before the coordinator ran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 tasks done
The window wrapper starts a budget window's next years with the observability context the coordinator's runtime captures when they are started. The test now gives the coordinator a runtime with its own capture and checks that the wrapper receives that capture, uncalled. Seen failing with the capture replaced by one that returns nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget-window store marks a window running, advances it and settles it as the Modal worker role, through report_run_tables' mapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…open The real runtime captures the trace of the operation that is open when the capture runs, so the next years' calls carry the coordinator's trace only when the window captures it inside the coordinator's operation. The test's runtime now records whether that operation is open at the capture, and the wrapper's stand-in captures after the year's coordinator returns, as the wrapper does. Seen failing with the capture taken before the operation opens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
November 12, 2026 22:50
anth-volk
added a commit
that referenced
this pull request
Nov 12, 2026
Brings in the budget windows (#1054). Both sides' imports are kept; the call-refused answer is public under main's name with the re-run keyword; the report route dispatches budget windows as main does, with the public answer helpers. The budget-window service is generic over its store, and the re-run service extends it, so the app builds one service that creates, re-runs and starts budget windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Nov 12, 2026
…indows #1054 (budget windows) merged into main, and also made the POST /reports body a union. The merge keeps both report types: - one ReportRequest of all three types (economy, budget_window, household) in a new report_api_models module, replacing the two-type unions each branch had, and report_request_errors importing it; - create_report dispatching household reports to _household_report_answer and the other two to a new _economy_report_answer, which keeps the router function under Ruff's complexity limit; - response_model listing the three answers, and the route's summary and description naming all three; - both packages' exports and the Stage 12 persistence README keeping both sides. #1054's tests that used "household" as an unknown report type now use "not_a_report_type", and the unknown-type message lists the three types. The OpenAPI tests check the three-type discriminator and 201 answers, and the household request's fields in a test of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Nov 12, 2026
Brings in #1054's budget windows. The sweep's 15-minute age now also times its settling of idle windows, and the migration document and the sweep app's docstring say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1053
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Changelog
changelog.d/v2-budget-windows.added.mdandchangelog.d/v2-budget-window-capacity.changed.md.Summary
Budget-window reports on the detailed-save path. A budget-window report compares a reform with its baseline in each year of a window of 1 to 75 years. Its calculation is one
budget_window_runsrow with one report run per year; at most five years are under way at once. Each year's coordinator, when its year ends, starts the window's earliest waiting years, and the window is marked succeeded once every year has succeeded, or failed as soon as a year fails. This replaces the draft #982, which was built on the simulation service's own tables and scheduled its years through call reports, both since removed.Simulation service:
POST /reportswithreport_type: "budget_window"budget_window_size(1 to 75, ending by 2200) and otherwise the same fields as an economy report, exceptinclude_cliffs, which is refused (422).POST /reportstells the two apart byreport_type(a discriminated body). A refused body's 422 locations are exactly what an economy report's were before (ReportRequestRouteremoves the type Pydantic adds), because the Public API passes them to its callers.budget_window_sizeset andyearthe first year;inputsholds{"include_cliffs": false}plusspmwhen given, the same as a single-year report without cliffs, so reuse (which comparesinputs) works both ways between windows and single-year reports. Onebudget_window_runsrow (pending, triggerinitial,recalculatefalse, the default worker's catalog version). For each year, a report run naming the window run and the year, on the same versions. Each year has its own baseline and reform simulations of that year, unless reuse finds succeeded runs of that year (find_reusable_runper year; the two sides never share a run). The report names the first year's simulations.BudgetWindowStore.advance_window), which starts the earliest five years under the window run's row lock. The first year's coordinator input is built as a single-year report's (report_run_calculation); each later year's is derived from it (year_calculation): only the year, its three runs, its artifact prefix and its call's creation time differ. The first years share one ten-second wait for Modal, counted from the first start: a later year not confirmed within it is deferred (YearStartDeferred) and waits for a started year's coordinator.report_id,report_run_id(the first year's run),budget_window_run_id,statusand every year's report run and simulation runs. A year Modal refuses, or a first year it doesn't confirm in time, fails the window and its waiting years, and answers 503modal_call_refusednamingreport_id. Otherwise the economy report's error rules apply (400 and 422 before any write, 500internal_error, no exception text indetail). The Public API's adapter readsreport_idandbudget_window_run_idand ignores the rest (agreed with tracker task C1 · 14)./readynow requires the service role'sUPDATEonbudget_window_runs'status,error_message,completed_atandupdated_at, which Let the simulation roles update a budget-window run's status policyengine-api#4267 grants.GET /reports/{report_id}on a window answers one of its years' runs; the window's own status is inbudget_window_runs, which the Public API reads.Stage 12 side (Modal coordinator, workers and sweep)
BudgetWindowStore(Stage 12 persistence library): an advance locks the window run's row, then fails the window and its waiting years (and their unstarted simulation runs) when a year failed, marks it succeeded when every year succeeded, or starts the earliest waiting years until five are under way, recording each call while the year is still pending with no call. A start Modal refuses fails that year and the window, unless the year's own call claimed it meanwhile. The Modal worker role and the service role may both advance a window.coordinate_reportruns every call throughcoordinate_window_year: it looks the year's window up once, marks the window running before the coordinator runs, and advances it after the coordinator returns, fails or is cancelled, starting the next years in the same Modal application (ModalYearStarter). A pre-emption advances nothing; the rerun advances when it ends, even when it finds its year already finished. A report run outside any window makes no window writes, and no window write changes a year's own outcome.budget_window_stalled).year_calculationandreport_run_artifact_prefixinstage12_execution, shared by the service and the coordinator; the artifact environment check lives there once.From #982
Ported: the cap of five, the year order, "the first failed year fails the window", the container-limit test. Dropped: the simulation service's own tables and revision 0005, call reports and their safety-check scheduling, totals summed on read (the Public API's, C1 · 14), and
/v2/budget-window-reports.Builds on, and what merges first
POST /simulationsandPOST /reports) have merged intomain, and this branch has mergedmainin.budget_window_runsupdates. It must be deployed before this merges. Until then, the service's/readyfails, and the coordinator's and sweep's window writes are refused.libs/policyengine-simulation-persistence/public-api-commitpins #4267's merge commit, the head of the Public API'smaster. Its v2 migrations and privilege manifest are the same as at the #4267 head the store and records tests ran against.Environment variables and secrets
No new variable is read.
SIMULATION_SERVICE_DATABASE_URLSIMULATION_SERVICE_DATABASE_URL_SECRET_NAMEadd_runtime_secret); the window store uses the service's existing engineMODAL_WORKER_DATABASE_URLpolicyengine-modal-worker-databasecoordinate_report(report_run_writer_secrets) and to the sweep'ssweep_stuck_runs(secrets=[...]), both withrequired_keysstagingandmain(B1 · 7). Unverified: the role holds #4267's grants.MODAL_TOKEN_ID,MODAL_TOKEN_SECRET,SIMULATION_V2_MODAL_ENVIRONMENT,SIMULATION_V2_MANIFEST_NAME,V2_SIMULATION_OUTPUT_BUCKETA coordinator starts the next year's call with
modal.Function.from_name(...).spawn, using the credentials Modal's container provides, as it starts its workers.Testing
REPORT_RUN_TEST_DATABASE_URL) skip in CI, as Coordinate economy runs through the run tables instead of the evaluation records #1051's report-run tests do: no workflow sets that variable.review-code): rounds 1 to 3 and two short rounds on later fixes. Every substantiated bug was fixed test-first; the reports are in the session's notes.🤖 Generated with Claude Code