Skip to content

fix(raven-oncall): a campaign bills only its own jobs under a shared rounds directory - #553

Merged
0xKT merged 4 commits into
mainfrom
fix/campaign_spend_counts_own_jobs
Sep 20, 2026
Merged

0xKT merged 4 commits into
mainfrom
fix/campaign_spend_counts_own_jobs

Conversation

@silverLXT

Copy link
Copy Markdown
Contributor

A campaign asked the shared rounds directory how much had been spent, and the
directory answered for every job in it, including jobs its sibling campaigns
had submitted. Rounds that keep one directory therefore bill each other, and
the bill is cumulative: round three opens already charged for rounds one and
two.

Measured on a run of 2026-09-11. Five campaigns declared the same rounds
directory. Round two had a budget of 130 and had actually used 34.6, but the
eighteen jobs the earlier rounds had left in the directory brought the reading
to 125.6, so the remaining 4.4 would not cover one more trial at 5.9 and the
round stopped. 142 GPU-minutes went unspent and that round's multi-seed
conclusion was lost. A sibling run that gave each round its own directory was
not affected, which is why this reads as a directory-hygiene problem until you
look at the meter.

Two changes. Spend is now counted from the campaign's own ledger, so a job a
sibling submitted is not billed here; the filter sits above the existing
timeline arithmetic, so same-device overlap is still charged once and two
cards in parallel are still charged twice. And declare refuses a rounds
directory a live sibling campaign already keeps, naming the sibling, so the
two rounds do not land in one directory in the first place.

Type: fix

Verification:

  • All oncall tests: 775 passed, 1 failed. The one failure
    (test_the_products_tool_face_equals_the_forks_config_intent, a missing
    optional a2a_client dependency on this machine) also fails on 0464aa1, so
    this adds six passing tests and no new failures.
  • The six new ones pin: a sibling's job is not billed; the campaign's own two
    trials overlapping on one device are still billed 20 minutes once; a job
    this executor just submitted is billed before the ledger has caught up (no
    refund window); billing_only leaves a foreign backend untouched; declare
    refuses a directory a live sibling keeps; a concluded sibling, and the
    campaign itself, do not count as a clash.
  • The first test also pins the measured number: the same fixture directory
    reads 125.6 unfiltered, which is 34.6 of its own plus 91.0 of its siblings.

Risk: low for the meter, moderate for declare. The meter can only ever bill
less than before, and a run that already gives each round its own directory
reads the same. Declare now refuses something it used to accept, so a caller
that deliberately shares a directory between live campaigns will see an error
naming the sibling; concluded siblings and re-declaring the same campaign are
both still fine.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: apply campaign ownership to OpenFOAM spend and prevent stale job adoption from concluded sibling directories.

Two concrete failures remain:

  • OpenFoamExecutor.spent_minutes() overrides the filtered implementation and still charges every job under the shared directory.
  • Reusing a concluded sibling's directory can collide on the deterministic idempotency key, causing the new campaign to inherit both an old bill and an old result.

Review coverage: I read AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and the applicable runtime context; inspected the full diff and all three commits; traced backend construction and spend callers, the watcher, ledger semantics, config-key generation, and submit/restart behavior; checked backward compatibility and the resource-admission architecture; and checked that existing tests were not weakened. git diff --check is clean. uv run pytest tests/test_agents_oncall_flow_*.py -q passed all 758 tests, but focused reproductions exposed the two gaps marked inline.

Comment thread agents/raven-oncall/plugins/oncall-flow/oncall_flow/tools/ops_declare.py Outdated
xiaotian.luo and others added 3 commits September 20, 2026 17:52
…rounds directory

`ProcessExecutor.spent_minutes` walked `{remote_dir}/jobs/*` and placed every
job it found on the campaign's timeline. Campaigns of one task usually declare
the same `remote_dir` -- an agent running a task in rounds points each round at
the same `runs/` -- so a later campaign was charged for every earlier one's runs
the moment it started. Measured 2026-09-11 on a shared 2xA800 box: five
campaigns shared one directory; the second-round campaign (budget 130) read
125.6 spent on its first look, 34.6 of it its own, and stopped with 142 real
minutes unspent and fifteen runs never submitted.

A job directory carries no campaign identity (its name's suffix is a digest of
the config), so ownership is read from the campaign's ledger. The executor
gains `restrict_spend_to(ledger)`; rows for jobs not in the ledger are skipped
before the timeline arithmetic, so the overlap rules for the campaign's own
concurrent runs are unchanged. Jobs this executor submitted itself are always
counted, so a run is billed before the ledger records it. `billing_only` in
`backends.py` hands the ledger over at the one seam every tool builds its
backend through; factories keep their one-argument shape, and a backend without
the seam (or a test double) is handed back untouched.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…campaign keeps

Even with spend read from the ledger, two campaigns writing rounds into one
directory still mix their job trees. `ops_declare` now refuses a `remote_dir`
that another campaign under the same ops home, not yet concluded, already
declared. A concluded sibling is done writing there and does not count, and
re-declaring the same campaign is not a collision. The refusal names the
campaign holding the directory and says what to do, in the same voice as the
rounds-inside-the-case refusal beside it.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The resource-admission spec described the spend measure as a walk of the jobs
directory; it now says the walk is filtered to the campaign's own ledger and
why. The changelog carries the fix under Unreleased.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@silverLXT
silverLXT force-pushed the fix/campaign_spend_counts_own_jobs branch from 250bbde to d794cda Compare September 20, 2026 09:52

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the two existing review threads still reproduce and must be addressed.

This revision's only delta replaces the ledger attribute probe with runtime isinstance narrowing; I traced its callers and found no separate regression in that change. It does not alter either outstanding failure path, and both focused reproductions still fail with the same measured results.

Coverage for this revision: I rechecked the repository rules and applicable context, the full branch diff and new delta/history, backend and tool callers, ledger/config-key/restart compatibility, the resource-admission architecture, and test coverage; no existing tests were weakened. git diff --check github/main...HEAD is clean. uv run pytest tests/test_agents_oncall_flow_*.py -q passed all 758 tests. The original two threads remain open because neither is settled.

…tovers

Two gaps in the ownership filter, both found in review.

OpenFoamExecutor inherits restrict_spend_to, so billing_only appears to
configure it, but it overrides spent_minutes and its own loop walked every
scanned row. An owned 20-core-minute case beside a sibling's 80 read 100.
It now asks the same question the base loop does, before it records the
width: _cores and the timeline are both read back by the caller, and a
sibling in either one is the same cross-campaign bill somewhere else.

A concluded sibling's directory was treated as free, but concluding does not
remove its jobs. A job directory is named from a digest of its config and
carries no campaign, so two campaigns running the same trial name the same
directory: the newcomer's ledger records that key as its own, the filter
reads the predecessor's minutes as this campaign's, and submit finds the old
result.json and calls the trial done instead of running it. A concluded
sibling now keeps its directory as long as its ledger holds a record; one
that never submitted anything left nothing to adopt and still steps aside. A
ledger that cannot be parsed counts as occupied, which is the safe direction.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@silverLXT
silverLXT force-pushed the fix/campaign_spend_counts_own_jobs branch from 7050e45 to 5ed0d5d Compare September 20, 2026 10:06

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

The new commit fixes both prior findings and adds direct regressions for them. I replied to and resolved both threads after verifying the fixes.

Coverage: I rechecked the repository rules and applicable context, the full branch diff and revision history, all affected backend/tool callers, ledger and deterministic-key behavior, restart/backward-compatibility paths, the resource-admission architecture, and the tests for weakening or missing assertions. I found no new defect. git diff --check github/main...HEAD is clean, and uv run pytest tests/test_agents_oncall_flow_*.py -q passed all 761 tests.

@silverLXT
silverLXT requested a review from 0xKT September 20, 2026 10:25
@0xKT
0xKT merged commit 0cb8b79 into main Sep 20, 2026
25 checks passed
@0xKT
0xKT deleted the fix/campaign_spend_counts_own_jobs branch September 20, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants