Summary
On 0.1.4, a transient filesystem race during the auditor read-only-guard snapshot aborts the entire run with provider_provider_error, even though the agent runtime itself is healthy. On 0.1.3 the same situation only failed the current round, which was then retried.
What happens
The auditor guard takes a workspace manifest before and after the auditor episode (snapshot_workspace in src/lh_harness/adapters/claude_permissions.py). If the workspace contains a directory that changes while the walk is in progress — the typical case is a build/cache directory such as a Rust target/ being rewritten by an incremental build — the walk records an OSError for the affected entries, e.g.:
OSError: [Errno 9] Bad file descriptor
The adapter then rejects the audit fail-closed, which is reasonable:
Auditor workspace read-only guard could not inspect every path; the audit was rejected fail-closed.
But because the episode result now carries status="error", the classifier introduced in 0.1.4 (classify_agent_runtime_failure in src/lh_harness/provider_errors.py) treats it as a terminal agent-runtime failure. None of the specific patterns (auth, quota, rate limit, network, model) match, so it falls through to the generic provider_error kind and the run is aborted with abort_reason=provider_provider_error.
So a local, transient audit-validity problem — the guard could not take a clean snapshot — is escalated into a fatal provider failure for the whole run. In a long multi-hour run this is expensive: everything stops over a filesystem race that would likely not recur on the next round.
Reproduction
Observed twice in real multi-hour runs (agent claude_code, Linux) on a Rust workspace, where the executor's cargo builds keep target/ churning and a snapshot walk overlapping such churn eventually hits the race.
A deterministic reproduction of the escalation (without waiting for the race itself):
- Take any
EpisodeResult whose metadata contains a non-empty verifier_workspace_snapshot_errors list and whose error ends with the fail-closed guard message above (this is exactly what ClaudeCodeAdapter.run_episode produces when the snapshot diff reports errors).
- Pass it to
classify_agent_runtime_failure.
- It returns an
AgentRuntimeFailure with abort_reason="provider_provider_error" — which the run loop treats as terminal — instead of leaving the failure to the normal round-retry path.
To provoke the underlying race itself: run with an auditor role on a workspace where a large directory is concurrently being rewritten during the auditor episode (e.g. a background loop that keeps creating/removing many files in a subdirectory while the audit runs). The before/after snapshot_workspace walk over that directory eventually returns verifier_workspace_snapshot_errors.
Expected behavior
The fail-closed rejection of the audit is fine, but its blast radius should be the round (fail + retry, as on 0.1.3), not the run. A guard snapshot failure is not an agent-runtime/provider failure.
Environment
- lh-harness 0.1.4 (
pip/uv install)
- Linux x86_64
- agent:
claude_code, auditor role with the read-only guard active
Summary
On 0.1.4, a transient filesystem race during the auditor read-only-guard snapshot aborts the entire run with
provider_provider_error, even though the agent runtime itself is healthy. On 0.1.3 the same situation only failed the current round, which was then retried.What happens
The auditor guard takes a workspace manifest before and after the auditor episode (
snapshot_workspaceinsrc/lh_harness/adapters/claude_permissions.py). If the workspace contains a directory that changes while the walk is in progress — the typical case is a build/cache directory such as a Rusttarget/being rewritten by an incremental build — the walk records anOSErrorfor the affected entries, e.g.:The adapter then rejects the audit fail-closed, which is reasonable:
But because the episode result now carries
status="error", the classifier introduced in 0.1.4 (classify_agent_runtime_failureinsrc/lh_harness/provider_errors.py) treats it as a terminal agent-runtime failure. None of the specific patterns (auth, quota, rate limit, network, model) match, so it falls through to the genericprovider_errorkind and the run is aborted withabort_reason=provider_provider_error.So a local, transient audit-validity problem — the guard could not take a clean snapshot — is escalated into a fatal provider failure for the whole run. In a long multi-hour run this is expensive: everything stops over a filesystem race that would likely not recur on the next round.
Reproduction
Observed twice in real multi-hour runs (agent
claude_code, Linux) on a Rust workspace, where the executor'scargobuilds keeptarget/churning and a snapshot walk overlapping such churn eventually hits the race.A deterministic reproduction of the escalation (without waiting for the race itself):
EpisodeResultwhosemetadatacontains a non-emptyverifier_workspace_snapshot_errorslist and whoseerrorends with the fail-closed guard message above (this is exactly whatClaudeCodeAdapter.run_episodeproduces when the snapshot diff reports errors).classify_agent_runtime_failure.AgentRuntimeFailurewithabort_reason="provider_provider_error"— which the run loop treats as terminal — instead of leaving the failure to the normal round-retry path.To provoke the underlying race itself: run with an auditor role on a workspace where a large directory is concurrently being rewritten during the auditor episode (e.g. a background loop that keeps creating/removing many files in a subdirectory while the audit runs). The
before/aftersnapshot_workspacewalk over that directory eventually returnsverifier_workspace_snapshot_errors.Expected behavior
The fail-closed rejection of the audit is fine, but its blast radius should be the round (fail + retry, as on 0.1.3), not the run. A guard snapshot failure is not an agent-runtime/provider failure.
Environment
pip/uvinstall)claude_code, auditor role with the read-only guard active