fix(evaluator-sdk): make Harbor's per-trial resume reachable with agent_dir set - #964
Merged
ngoncharenko merged 3 commits intoJul 30, 2026
Conversation
…nt_dir set scoped_harbor_agent_import minted the synthetic package name with a fresh uuid4 on every call. That string lands in AgentConfig.import_path and therefore in Harbor's JobConfig, which Harbor compares field-by-field before resuming a job directory, so the comparison failed on every rerun and Harbor raised FileExistsError instead of resuming. Its per-trial resume was unreachable for any caller that sets agent_dir -- which is every Experimentalist run. Derive the suffix from a content digest of agent_dir instead. Identical contents now share a package name, so the sys.modules injection is refcounted and torn down on the last scope exit rather than the first. The digest takes the same jobs_dir exclusion the cache stamp uses, so a jobs_dir nested under agent_dir cannot shift the import path as results accumulate. Dropping the unconditional discard exposed a second problem. The SDK cache stamp is deliberately looser than Harbor's JobConfig comparison -- quiet, n_concurrent_trials and the task_names filter change the JobConfig without changing the results, and keying on them would discard completed candidates over a core-count difference -- so a directory that passes the stamp can still be refused, and Harbor refuses by raising. _build_native_job now identifies that refusal positively (bare message, no errno, naming this job dir), answers it by discarding and re-running, and logs which field forced it. Anything else propagates untouched rather than costing a job directory. Adds a drift guard that fails when a Harbor upgrade adds a compared JobConfig field the stamp does not classify, a pin on the refusal wording the predicate matches, and a real-Docker e2e for the resume this change exists to enable. Fixes AALGO-430. Refs AALGO-427. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Nick Goncharenko <ngoncharenko@nvidia.com>
Generated by `make vendor`; import-path rewrites only, no hand edits. Refs AALGO-430. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Nick Goncharenko <ngoncharenko@nvidia.com>
Contributor
Author
|
@claude review the PR |
Contributor
Author
|
@coderabbitai review the PR |
Contributor
|
✅ Action performedReview finished.
|
Contributor
📝 WalkthroughWalkthroughHarbor native jobs now preserve reusable directories, retry only Harbor-specific resume refusals, and use deterministic content-addressed agent imports with refcounted teardown. Unit tests cover cache, import, error, and config-drift behavior; an end-to-end test validates resuming a partial custom-agent job. ChangesHarbor resume behavior
Sequence Diagram(s)sequenceDiagram
participant HarborAgentTaskRunner
participant HarborJob
participant job_dir
HarborAgentTaskRunner->>HarborJob: create or run native job
HarborJob-->>HarborAgentTaskRunner: FileExistsError
HarborAgentTaskRunner->>HarborAgentTaskRunner: identify Harbor resume refusal
HarborAgentTaskRunner->>job_dir: delete refused directory
HarborAgentTaskRunner->>HarborJob: rerun native job
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Two of the four A2 cases were already covered (multi-attempt dir naming, non-`reward` metric keys). These close the other two, which the smoke run never exercised: - Trial dir with no result.json — Harbor still creates the dir when a trial dies before the verifier. Assert it is skipped rather than raising, and that its task is still reported missing instead of silently vanishing. Same for a truncated result.json. - exception_info shapes beyond a bare string. Harbor writes a mapping; only the bare-string path was tested. Parametrized over exception_type/type/name/class, a mapping with none of those (-> UnknownException), an empty mapping, a bare string, and a non-string scalar — plus the absent case, which gives the rest their meaning. Resolving any of these to None would promote a crashed trial to COMPLETED and let it score. Verified the parametrization is load-bearing: disabling the Mapping branch of _exception_type fails 6 of its 8 cases. No production code changed; `make vendor` produces no diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Nick Goncharenko <ngoncharenko@nvidia.com>
ngoncharenko
merged commit Jul 30, 2026
a25a06c
into
ngoncharenko/aalgo-312-wire-eval-into-optimizer
4 checks passed
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 AALGO-430. Refs AALGO-427.
Targets
ngoncharenko/aalgo-312-wire-eval-into-optimizer, which this branch builds on.The bug
scoped_harbor_agent_importminted the synthetic package name with a freshuuid4()on every call. That string becomesAgentConfig.import_pathand so part of Harbor'sJobConfig, which Harbor compares field-by-field before resuming a job directory. The comparison therefore failed on every rerun, Harbor raisedFileExistsErrorinstead of resuming, and its per-trial resume was unreachable for any caller that setsagent_dir— which is every Experimentalist run.AALGO-427 had papered over this by discarding the job directory on every cache miss (
force_rerunwheneveragent_dirwas set). That removed the crash by never resuming.Example
Run 1
Agent unchanged. UUID happens to be deadbeef:
Harbor writes jobs/resume-job/config.json with that path, finishes trial 1 of 2, then the process exits mid-run (or you only asked for 1 attempt and later bump to 2).
Run 2 - same agent files, same job dir
New UUID cafef00d:
Harbor compares the new JobConfig to the saved one:
→ configs differ → Harbor raises FileExistsError: ... resumed with a different config
→ cannot resume; completed Docker work is stranded.
Nothing about the agent changed. Only the random suffix did.
After the fix
Suffix = content digest of agent_dir:
both runs, identical agent files:
Same contents → same path → Harbor’s equality check passes → missing trials resume.
Edit the agent (wrapper.py changes) → digest changes → path changes → Harbor correctly refuses resume (stale results for a different agent).
Why UUID was there originally?
UUID avoided collisions if two different agent folders named agent were imported in one process. That isolation goal was valid; the bug was making the name part of a persisted config that must be stable across reruns. Content-addressing keeps isolation and stability.
Before the fix, Experimentalist always set agent_dir, so every eval with a custom agent hit this path and could never use Harbor’s per-trial resume.
The fix
Content-addressed import path. The suffix now comes from a content digest of
agent_dir, so an unchanged agent produces a stableJobConfigand an edited one invalidates the job dir on Harbor's own terms. Identical contents share a package name, so thesys.modulesinjection is refcounted and torn down on the last scope exit. The digest takes the samejobs_direxclusion_cache_stampuses, so ajobs_dirnested underagent_dircan't shift the import path as results accumulate.Handling Harbor's refusal. Removing the unconditional discard exposed a second problem: the SDK cache stamp is deliberately looser than Harbor's
JobConfig.__eq__.quiet,n_concurrent_trialsand thetask_namesfilter change the JobConfig without changing the results, and keying on them would discard every completed candidate when an experiment resumes on a box with a different core count (the Experimentalist defaultsn_concurrent_trialstoos.cpu_count()). So a directory that passes the stamp can still be refused — and Harbor refuses by raising._build_native_jobnow identifies that refusal positively — bare message, noerrno, naming this job dir — discards, re-runs, and logs which field forced it (n_concurrent_trials: 10 -> 4). Any otherFileExistsErrorpropagates untouched rather than costing a job directory.Why not align the config instead
Mirroring Harbor's compared set would fix the crash at the source but regress two intentional behaviours: a core-count change would discard completed candidates, and keying on
task_nameswould break subset-of-a-cached-job hits that_stamp_coverageexists to support. The stamp answers "did these inputs produce these results?"; Harbor answers "can I resume this directory?" — different questions, and the SDK's is the right one to drive a cache. The divergence is now documented and guarded rather than removed.Tests
uuid4makes it fail with the originalFileExistsError)EEXIST, and a refusal naming a different job dirFileExistsErrormust propagate and leave the directory intactJobConfigfield must be classified as Harbor-ignored, never-set-by-the-SDK, stamp-covered, or knowingly-unkeyed-with-a-reason — a Harbor upgrade adding a compared field fails the buildAll guards mutation-checked: each fix reverted individually makes the intended test fail.
Verification
The vendored SDK is a separate commit, regenerated with
make vendor— import-path rewrites only.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests