Repository navigation
DRU-989 -- A run waiting for the usage window shows as running - #1075
Conversation
79a4fbd to
8ceaae6
Compare
8ceaae6 to
4da0b93
Compare
czpython
left a comment
There was a problem hiding this comment.
The core fix is right. The spend-limit marker now comes before session limit, and write_cost runs before check_returncode. Please make these changes before merge:
-
Every phase push now records two DBOS operations (blocker).
set_run_phasealways sendsphase_resets_atbeforephase. DBOS records eachset_eventas an operation and checks its name on replay. A run that is active at deploy time, for example a build parked at a gate, replays with shifted operation ids and fails withDBOSUnexpectedStepError.reads.pyalready ignores the reset outsidewaiting_for_quota, so the clearing write is not necessary. Sendphase_resets_atonly whenresets_atis given. -
Remove the ISO round trip. Druks uses the DBOS pickle serializer, so a
datetimepasses through step results and events unchanged. Return(wait_seconds, reset)from_quota_retry_wait. Then remove the dict,float(...),str(...)and theisoformat/fromisoformatcalls inagents.pyandactivity.py.get_run_phase_resets_atcan return the event directly, and the test does not needclock.fromisoformat.side_effect. -
agents.py:quota_resets_at = Noneruns on everyHarnessError, andRetry.QUOTAis checked a second time after the chain. Moveset_run_phase(...)into theelif Retry.QUOTAbranch, after the max-wait raise. -
harnesses/base.py: the comment change is not part of the fix. Revert it. -
The marker and the description do not agree. The description says
spend limitand the code saysmonthly spend limit. Use one of the two in both places. -
Tests:
- Delete
test_run_phase.py. It tests a fake DBOS dict. - Remove the cost assertions in
test_a_carried_failure_is_raised_with_its_code. They test behavior that already exists:sandbox/host.pyreads the cost after a failure, andAgentCall.finishrecords it. - In
test_failed_claude_call_writes_cost_before_raising, keep only the cost assertions.codeandretryare class constants, andtest_sandboxed_harness.pyalready covers the classification.
- Delete
-
Frontend (minor):
phaseResetsAt: string | null | undefined = undefinedhas a default that is not necessary.?:is sufficient. You can putwaiting_for_quotainPHASE_LINESand add a special case only when a reset time is present.
91c789c to
2b21b9c
Compare
2b21b9c to
23816c0
Compare
czpython
left a comment
There was a problem hiding this comment.
Thanks, the patch is gone. Please make three more changes before merge:
-
Rename
phase_resets_attophase_ends_atand store the wake time.phaseis a generic field. The page needs the time when the run tries again, not the time when the provider resets the window. Set the value after the jitter, todatetime.now(UTC) + timedelta(seconds=wait_seconds). Then:- The 30-minute fallback also gets a time. Today it sends no reset.
_quota_retry_waitcan return only the wait again, as on main.- Do not use
expires_at. In Druks it means that a token, grant, secret or lease stops being valid.
-
An old reset shows on a later wait.
quota_retriedresets for each agent call, so one run can wait for quota more than once. If the second wait has no reset, onlyphaseis sent.phase_resets_atkeeps the first value, which is already in the past, and the page shows that time. With point 1, every quota wait writes an end time, and an old end time is always in the past. Soreads.pycan readphase_ends_atwhile the run is running and return it only when it is later than now. Thephase == "waiting_for_quota"check is then not necessary. -
A failed call still loses its cost in Codex, Pi and OpenCode. The issue expects every failed call to record its cost. The PR fixes only Claude. Each of the other harnesses raises before
write_cost:- Codex:
check_returncoderuns first, and the cost is read afteroutput.json, which a failed run may not have. Read the cost fromsession.jsonland write it first. The reader already returnsNonewhen the file is missing. - Pi:
check_returncoderuns first. A provider error that exits with zero raises inside the message loop, before the usage sum. Add up the usage and write the cost, then raise. A broken stream needscheck_returncodein theexcept, as in Claude. - OpenCode:
info.errorraises before the cost sum, andcheck_returncoderuns beforewrite_cost. Use the same fix as for Pi.
Add to the base
parsedocstring that the cost sidecar is written before a failure raises. Add one cost-before-raise test for each harness, next to the Claude test intest_cost_capture.py. - Codex:
This comment was marked as low quality.
This comment was marked as low quality.
This comment was marked as resolved.
This comment was marked as resolved.
c0864fb to
54d201d
Compare
czpython
left a comment
There was a problem hiding this comment.
Thanks, all three points are fixed. Please make two small changes before merge:
-
Pi: the usage guard serves only the tests.
if message.get("usage")and the two... if usage else Noneexist because two error fixtures intest_pi.pysend nousage. A real Pi assistant message always hasusage. Addusageto those two fixtures, readmessage["usage"]directly, and remove both conditionals. -
reads.py(minor): the secondlatest and latest.is_runningcan bephase. You can also remove the reassignment toNoneand put the check in theSubjectResponse(...)call:phase_ends_at=ends_at if ends_at and ends_at > datetime.now(UTC) else None.
54d201d to
d32e980
Compare
What changed
Fixes #981.
Claude's monthly spend-limit message also contains
session limit resets....Druks was matching
session limitand treating the error like a normal quota window. It shut down the VM, waited for the next window, and retried. But the monthly spend limit was still there, so the retry only wasted another usage window.Now:
monthly spend limitis checked beforesession limit, and the call fails directly.I kept the existing
phasestring and addedphaseResetsAtseparately, so the existing field does not change shape.No database migration, dependency, or lockfile change.
This does not preserve uncommitted files from the VM. That is a separate problem.