Skip to content

feat(opensandbox): add automatic job attribution (team/user/workload) to sandbox metadata - #2020

Merged
ananthsub merged 3 commits into
mainfrom
hemil/opensandbox-attribution
Jul 31, 2026
Merged

feat(opensandbox): add automatic job attribution (team/user/workload) to sandbox metadata#2020
ananthsub merged 3 commits into
mainfrom
hemil/opensandbox-attribution

Conversation

@hemildesai

@hemildesai hemildesai commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes RL-1013 (Linear issue). Every sandbox created through the OpenSandbox provider now automatically carries nemo-gym.nvidia.com/team / nemo-gym.nvidia.com/user / nemo-gym.nvidia.com/workload / nemo-gym.nvidia.com/run keys in its metadata. OpenSandbox propagates sandbox metadata as Kubernetes labels on the sandbox resources (BatchSandbox CR + pod), so sandboxes are attributable both through the OpenSandbox list API and directly at the cluster level:

kubectl get pods -l nemo-gym.nvidia.com/team=my-team

Why metadata (SDK research)

From the opensandbox SDK (0.1.9) source, there are exactly three candidate carriers for attribution:

Carrier Verdict
Sandbox.create(metadata=...) Chosen. Becomes Kubernetes labels on the sandbox (server enforces K8s label rules: values ≤63 chars, opensandbox.io/ prefix reserved for the platform itself), is queryable via SandboxManager.list_sandbox_infos(SandboxFilter(metadata={...})), and is patchable post-create (PATCH /v1/sandboxes/{id}/metadata).
extensions ❌ Opaque provider pass-through, not queryable, immutable post-create.
ConnectionConfig.headers / user_agent / api_key ❌ Per-request HTTP headers only; not stored on the sandbox.

K8s annotations are not settable through the SDK/API at all — labels are the only k8s-level channel it propagates. Keys use the Kubernetes prefixed-key convention (nemo-gym.nvidia.com/) for namespacing and provenance, matching the existing <project>.nvidia.com/ label-key convention used by run-level sandbox tooling; the prefix is configurable (key_prefix: "" restores bare team/user/workload keys).

The provider already sanitizes metadata values to K8s label rules (_metadata_value: charset replacement + 63-char truncation) and pipes spec.metadata into Sandbox.create, so this change only adds the automatic injection.

Design

  • nemo_gym/sandbox/attribution.py (shared, provider-agnostic so docker/openshell/ecs can adopt it later): resolve_attribution() resolves each field in order — explicit config → NEMO_GYM_TEAM / NEMO_GYM_USER / NEMO_GYM_WORKLOAD env vars → Slurm job env vars (SLURM_JOB_ACCOUNT / SLURM_JOB_USER / SLURM_JOB_NAME) → OS login name (user only; root is ignored since containers default to it) → the gym CLI's NEMO_GYM_CONFIG_PATH server instance name (workload only). Unresolvable fields are omitted, never guessed. A run key (NEMO_GYM_RUN_ID, else generated once per process and logged at first create) scopes sandboxes to one launch so an interrupted run's sandboxes can be listed and garbage-collected exactly.
  • OpenSandboxProvider: new attribution config group (enabled: true by default; team/user/workload overrides; key_prefix for the label-key namespace). create() merges attribution under the spec's metadata, so explicit sandbox_spec.metadata / default_metadata keys always win; values then flow through the existing K8s-label sanitization.
  • Documented in configs/opensandbox.yaml and fern/.../sandbox/opensandbox.mdx (new Job Attribution section).

Testing

  • tests/unit_tests/test_sandbox_attribution.py: resolution precedence (config > NEMO_GYM_* > Slurm > login name), blank-value handling, omission when unresolvable, os.environ default.
  • tests/unit_tests/test_opensandbox_provider.py: attribution lands in the SDK create call's metadata with prefixed keys (and sanitization applied, e.g. nemo rlnemo_rl), explicit spec metadata beats attribution, config overrides beat env detection, enabled: false disables injection, and key_prefix variants (bare, custom, trailing-slash normalization).
  • Full unit suite: 1152 passed; the 6 failures are pre-existing terminal-formatting assertions that fail identically on main.

🤖 Generated with Claude Code

@copy-pr-bot

copy-pr-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview your docs: https://nvidia-preview-hemil-opensandbox-attribution.docs.buildwithfern.com/nemo/gym

Here are the markdown pages you've updated:

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview your docs: https://nvidia-preview-hemil-opensandbox-attribution.docs.buildwithfern.com/nemo/gym

Here are the markdown pages you've updated:

hemildesai and others added 3 commits July 31, 2026 10:32
… to sandbox metadata

Resolves RL-1013. Every sandbox created through the OpenSandbox provider now
carries team/user/workload attribution in its metadata, which OpenSandbox
stores as Kubernetes labels on the sandbox and exposes through the list API's
metadata filter (e.g. team=my-team), so cluster operators can attribute
running sandboxes to the job that created them.

- new nemo_gym/sandbox/attribution.py: resolve_attribution() resolves each
  field from explicit config, then NEMO_GYM_TEAM/NEMO_GYM_USER/
  NEMO_GYM_WORKLOAD env vars, then Slurm job env vars (SLURM_JOB_ACCOUNT/
  SLURM_JOB_USER/SLURM_JOB_NAME), then the OS login name for user;
  unresolvable fields are omitted rather than guessed. Shared module so
  other providers can adopt the same convention.
- OpenSandboxProvider grows an 'attribution' config group (enabled: true by
  default, plus team/user/workload overrides); create() merges attribution
  under the spec's metadata, so explicit sandbox_spec.metadata and
  default_metadata keys always win, and values flow through the existing
  Kubernetes-label sanitization.
- documented in configs/opensandbox.yaml and the OpenSandbox fern page.

Carrier choice (from SDK research): Sandbox.create(metadata=...) is the only
create-time channel that is stored on the sandbox, queryable (SandboxFilter
metadata), and patchable post-create; extensions are opaque/unqueryable and
connection headers are per-request only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
…labels

Attribution keys are now emitted as nemo-gym.nvidia.com/team|user|workload
(configurable via attribution.key_prefix; "" restores bare keys). OpenSandbox
propagates sandbox metadata as Kubernetes labels on the sandbox resources, so
prefixed keys give cleanly namespaced, cluster-level attribution:

    kubectl get pods -l nemo-gym.nvidia.com/team=my-team

matching the existing <project>.nvidia.com/ label-key convention used for
run-level sandbox tooling, and avoiding collisions with other tenants' labels
on shared clusters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
…ed labels

Address review findings from replaying attribution against a real
Kubernetes eval deployment, where the previous fallback chain resolved to
user=root with team/workload omitted:

- Ignore "root" from the OS-login fallback: containers default to root,
  so it attributes the image, not a person. Explicit NEMO_GYM_USER=root
  is still honored.
- Fall back workload to NEMO_GYM_CONFIG_PATH, the server instance name
  the gym CLI already sets on every server process it spawns, so workload
  resolves with zero plumbing outside Slurm.
- Add a run attribution key (NEMO_GYM_RUN_ID, else generated once per
  process and logged at first create) so one launch's sandboxes can be
  listed and garbage-collected exactly; team/user/workload cannot
  distinguish two runs of the same workload by the same user.
- Validate attribution.key_prefix as a DNS-1123 label-key prefix at
  config time instead of failing server-side at create with an opaque
  error.
- Docs: correct the "every sandbox" claim (fields are omitted when
  unresolvable) and add a Kubernetes deployment section showing how to
  set NEMO_GYM_* on pod specs directly or via the downward API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
@ananthsub
ananthsub force-pushed the hemil/opensandbox-attribution branch from 9824ec2 to 4b19975 Compare July 31, 2026 17:32
@ananthsub
ananthsub enabled auto-merge (squash) July 31, 2026 17:33
@ananthsub
ananthsub merged commit 967ae14 into main Jul 31, 2026
15 of 16 checks passed
@ananthsub
ananthsub deleted the hemil/opensandbox-attribution branch July 31, 2026 17:36
bxyu-nvidia pushed a commit that referenced this pull request Jul 31, 2026
…esource requests/limits (#2212)

## Summary

Hardens the OpenSandbox provider and the `mini_swe_agent_2` eval path so
that large sandboxed agent evals (SWE-bench Verified scale: hundreds of
concurrent rollouts against a Kubernetes-backed OpenSandbox deployment)
run end-to-end reliably: every finished rollout is delivered and
recorded, and sandbox-infrastructure noise is bounded instead of
silently zeroing rewards. Diagnosed on multi-node runs at concurrency
300–1500; the same failure signatures appear at concurrency 8, just less
often.

### 1. `Server disconnected without sending a response`

The SDK's default httpx pool keeps idle connections for 30s
(`opensandbox.config.connection.with_transport_if_missing`), but the
OpenSandbox server's uvicorn keep-alive reaper closes idle sockets after
~5s. Agent workloads idle between sandbox commands (model think time),
so commands routinely reuse a socket the server already closed —
surfacing as `httpx.RemoteProtocolError` → `SandboxInternalException`,
which permanently kills the rollout.

**Fix:** the provider injects a transport whose `keepalive_expiry` sits
*below* the server's keep-alive timeout (default 3s,
`connection.keepalive_expiry_s`):

- `connection.transport_backend: httpx` (default) — stock
`httpx.AsyncHTTPTransport(limits=..., retries=connect_retries)`. No new
required dependencies.
- `connection.transport_backend: aiohttp` (opt-in) — aiohttp pool under
the SDK's httpx surface via
[`httpx-aiohttp`](https://github.com/karpetrosyan/httpx-aiohttp); falls
back to the httpx transport with a warning when absent.
- `keepalive_expiry_s: null` disables injection entirely (SDK default
transport).

### 2. 502 `could not connect to the backend sandbox
endpoint='<podIP>:44772'` on first command

With `create.skip_health_check: true`, `Sandbox.create` returns before
the pod's exec daemon is listening; the first command races pod startup
and the server proxy 502s. Under a create burst this killed 7–12
rollouts per run.

**Fix:** `create.skip_health_check: false` (create waits for readiness,
bounded by the spec's `ready_timeout_s`; `create.timeout_s` kept above
the ready timeout).

**`command_retries` stays at 0.** Retrying a command the server may have
already started would execute it twice, and agent commands are
frequently mutating. The keepalive bound removes the stale-connection
failures retries were compensating for. Raise it only for idempotent
workloads.

### 3. Separate resource requests and limits

`Sandbox.create` accepts distinct `resource` (limits) and
`resource_requests` maps; a lone `resource` map is applied by the server
as requests=limits. This exposes the requests side via
`sandbox_spec.provider_options.resource_requests` (same keys as
`SandboxSpec.resources`). Motivation: SWE-bench test suites OOM-killed
sandbox pods at 2Gi, but raising a single combined map to 8Gi would 4×
the cluster reservation; with the split, limits rise while requests stay
small and pods pack densely. Requires `opensandbox>=0.1.15` (lower bound
raised here).

### 4. Sandbox teardown off the rollout's critical path

`env.cleanup()` ran in the rollout Ray task's `finally` block, so a
finished result only became fetchable after the sandbox DELETE returned
— and a teardown failure re-raised *over* the successful eval, degrading
it to a reward-0 error row. Teardown is also effectively one-shot
(`Sandbox.stop` latches `_closed` before dispatch), so a single failed
DELETE leaked the sandbox while still costing the rollout.

**Fix:** cleanup runs on a best-effort daemon thread; results return
immediately, teardown errors are logged rather than raised, and orphans
remain covered by the provider's sandbox TTL.

### 5. High-concurrency rollout delivery (`mini_swe_agent_2`)

Two coupled fixes so every finished rollout is delivered at high
concurrency (note: at very high concurrency the single-process
policy-model proxy remains a separate bottleneck — connection-refused
storms confirmed by a controlled A/B — with its scaling fix tracked
separately):

- **await the Ray ObjectRef** instead of `asyncio.to_thread(ray.get,
...)`: the default executor caps at `min(32, cpu+4)` threads, each
pinned for a full rollout, so delivery stalls at ~32 concurrent rollouts
and finished tasks queue behind blocked `ray.get` calls.
- **bounded litellm retries (`num_retries=5`, config-overridable)** — no
retry means one transient LLM-call failure kills a whole rollout;
unbounded retries make failures look like hangs — and
**`num_cpus=0.25`** on the rollout Ray task so concurrency is not capped
at cluster core count (rollouts are I/O-bound).

### 6. `datasets` declared in the base agent config

The struct-mode config merge rejects keys absent from the base config,
so a benchmark config using `_inherit_from` could not add its dataset
list (`ConfigKeyError: Key 'datasets' is not in struct`). Declared empty
in the base server config, matching how other agents (e.g. `swe_agents`)
expose it.

## Validation

- Requests/limits split (paired multi-node runs, identical except the
variable under test): infra-failed rollouts 41/316 (**13.0%**) at 2Gi
requests=limits → 19/288 (**6.6%**) at 8Gi limits / 2Gi requests; pass@1
58.0% → **67.1%**.
- Keepalive bound + health-checked create: zero `Server disconnected`
events and zero create-race 502s across all subsequent runs (previously
2–12 per run).
- Full stack, end-to-end from this branch: single-pass SWE-bench
Verified (500 instances, concurrency 500+) delivered **>98% of
rollouts** with ~1.6% residual sandbox-infra failures (server-side proxy
502s on established sandboxes, tracked separately) and zero
client-transport failures. Before the delivery fixes (#4/#5), an
identically shaped run lost the majority of *completed* evaluations at
the wall — hundreds of finished evals, only tens recorded.

## Relationship to #2020

Complementary, no overlap; #2020 (job attribution metadata) has since
merged and this branch is updated on top of it. Field-validated together
— the attribution labels are what make post-cancellation sandbox garbage
collection safely scoped to a single job.

## Testing

- `tests/unit_tests/test_opensandbox_provider.py`: transport-backend
coverage (httpx default with keepalive expiry, custom pool settings,
fallback when `httpx_aiohttp` is unavailable, `null` disables
injection), aiohttp opt-in (importorskip-guarded), and requests/limits
plumbing tests. 16 passed with and without `httpx-aiohttp`.
- `responses_api_agents/mini_swe_agent_2/tests/test_app.py`: updated for
the awaited ObjectRef (awaitable `FakeObjectRef`); assertions
strengthened to check the Ray call's params.
- CI green except a pre-existing `tau2` failure on `main` (reproduces on
unrelated PRs).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
OlegSudakov pushed a commit to OlegSudakov/Gym that referenced this pull request Aug 7, 2026
… to sandbox metadata (NVIDIA-NeMo#2020)

## Summary

Fixes RL-1013 ([Linear
issue](https://linear.app/nvidia/issue/RL-1013/add-job-attribution-teamuserworkload-to-opensandbox)).
Every sandbox created through the OpenSandbox provider now automatically
carries `nemo-gym.nvidia.com/team` / `nemo-gym.nvidia.com/user` /
`nemo-gym.nvidia.com/workload` / `nemo-gym.nvidia.com/run` keys in its
metadata. OpenSandbox propagates sandbox metadata as **Kubernetes
labels** on the sandbox resources (BatchSandbox CR + pod), so sandboxes
are attributable both through the OpenSandbox list API and directly at
the cluster level:

```bash
kubectl get pods -l nemo-gym.nvidia.com/team=my-team
```

## Why metadata (SDK research)

From the `opensandbox` SDK (0.1.9) source, there are exactly three
candidate carriers for attribution:

| Carrier | Verdict |
|---|---|
| `Sandbox.create(metadata=...)` | ✅ **Chosen.** Becomes Kubernetes
labels on the sandbox (server enforces K8s label rules: values ≤63
chars, `opensandbox.io/` prefix reserved for the platform itself), is
queryable via
`SandboxManager.list_sandbox_infos(SandboxFilter(metadata={...}))`, and
is patchable post-create (`PATCH /v1/sandboxes/{id}/metadata`). |
| `extensions` | ❌ Opaque provider pass-through, not queryable,
immutable post-create. |
| `ConnectionConfig.headers` / `user_agent` / `api_key` | ❌ Per-request
HTTP headers only; not stored on the sandbox. |

K8s **annotations** are not settable through the SDK/API at all — labels
are the only k8s-level channel it propagates. Keys use the Kubernetes
prefixed-key convention (`nemo-gym.nvidia.com/`) for namespacing and
provenance, matching the existing `<project>.nvidia.com/` label-key
convention used by run-level sandbox tooling; the prefix is configurable
(`key_prefix: ""` restores bare `team`/`user`/`workload` keys).

The provider already sanitizes metadata values to K8s label rules
(`_metadata_value`: charset replacement + 63-char truncation) and pipes
`spec.metadata` into `Sandbox.create`, so this change only adds the
automatic injection.

## Design

- **`nemo_gym/sandbox/attribution.py`** (shared, provider-agnostic so
docker/openshell/ecs can adopt it later): `resolve_attribution()`
resolves each field in order — explicit config → `NEMO_GYM_TEAM` /
`NEMO_GYM_USER` / `NEMO_GYM_WORKLOAD` env vars → Slurm job env vars
(`SLURM_JOB_ACCOUNT` / `SLURM_JOB_USER` / `SLURM_JOB_NAME`) → OS login
name (`user` only; `root` is ignored since containers default to it) →
the gym CLI's `NEMO_GYM_CONFIG_PATH` server instance name (`workload`
only). Unresolvable fields are **omitted, never guessed**. A `run` key
(`NEMO_GYM_RUN_ID`, else generated once per process and logged at first
create) scopes sandboxes to one launch so an interrupted run's sandboxes
can be listed and garbage-collected exactly.
- **`OpenSandboxProvider`**: new `attribution` config group (`enabled:
true` by default; `team`/`user`/`workload` overrides; `key_prefix` for
the label-key namespace). `create()` merges attribution **under** the
spec's metadata, so explicit `sandbox_spec.metadata` /
`default_metadata` keys always win; values then flow through the
existing K8s-label sanitization.
- Documented in `configs/opensandbox.yaml` and
`fern/.../sandbox/opensandbox.mdx` (new *Job Attribution* section).

## Testing

- `tests/unit_tests/test_sandbox_attribution.py`: resolution precedence
(config > NEMO_GYM_* > Slurm > login name), blank-value handling,
omission when unresolvable, `os.environ` default.
- `tests/unit_tests/test_opensandbox_provider.py`: attribution lands in
the SDK `create` call's `metadata` with prefixed keys (and sanitization
applied, e.g. `nemo rl` → `nemo_rl`), explicit spec metadata beats
attribution, config overrides beat env detection, `enabled: false`
disables injection, and `key_prefix` variants (bare, custom,
trailing-slash normalization).
- Full unit suite: 1152 passed; the 6 failures are pre-existing
terminal-formatting assertions that fail identically on `main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
OlegSudakov pushed a commit to OlegSudakov/Gym that referenced this pull request Aug 7, 2026
…esource requests/limits (NVIDIA-NeMo#2212)

## Summary

Hardens the OpenSandbox provider and the `mini_swe_agent_2` eval path so
that large sandboxed agent evals (SWE-bench Verified scale: hundreds of
concurrent rollouts against a Kubernetes-backed OpenSandbox deployment)
run end-to-end reliably: every finished rollout is delivered and
recorded, and sandbox-infrastructure noise is bounded instead of
silently zeroing rewards. Diagnosed on multi-node runs at concurrency
300–1500; the same failure signatures appear at concurrency 8, just less
often.

### 1. `Server disconnected without sending a response`

The SDK's default httpx pool keeps idle connections for 30s
(`opensandbox.config.connection.with_transport_if_missing`), but the
OpenSandbox server's uvicorn keep-alive reaper closes idle sockets after
~5s. Agent workloads idle between sandbox commands (model think time),
so commands routinely reuse a socket the server already closed —
surfacing as `httpx.RemoteProtocolError` → `SandboxInternalException`,
which permanently kills the rollout.

**Fix:** the provider injects a transport whose `keepalive_expiry` sits
*below* the server's keep-alive timeout (default 3s,
`connection.keepalive_expiry_s`):

- `connection.transport_backend: httpx` (default) — stock
`httpx.AsyncHTTPTransport(limits=..., retries=connect_retries)`. No new
required dependencies.
- `connection.transport_backend: aiohttp` (opt-in) — aiohttp pool under
the SDK's httpx surface via
[`httpx-aiohttp`](https://github.com/karpetrosyan/httpx-aiohttp); falls
back to the httpx transport with a warning when absent.
- `keepalive_expiry_s: null` disables injection entirely (SDK default
transport).

### 2. 502 `could not connect to the backend sandbox
endpoint='<podIP>:44772'` on first command

With `create.skip_health_check: true`, `Sandbox.create` returns before
the pod's exec daemon is listening; the first command races pod startup
and the server proxy 502s. Under a create burst this killed 7–12
rollouts per run.

**Fix:** `create.skip_health_check: false` (create waits for readiness,
bounded by the spec's `ready_timeout_s`; `create.timeout_s` kept above
the ready timeout).

**`command_retries` stays at 0.** Retrying a command the server may have
already started would execute it twice, and agent commands are
frequently mutating. The keepalive bound removes the stale-connection
failures retries were compensating for. Raise it only for idempotent
workloads.

### 3. Separate resource requests and limits

`Sandbox.create` accepts distinct `resource` (limits) and
`resource_requests` maps; a lone `resource` map is applied by the server
as requests=limits. This exposes the requests side via
`sandbox_spec.provider_options.resource_requests` (same keys as
`SandboxSpec.resources`). Motivation: SWE-bench test suites OOM-killed
sandbox pods at 2Gi, but raising a single combined map to 8Gi would 4×
the cluster reservation; with the split, limits rise while requests stay
small and pods pack densely. Requires `opensandbox>=0.1.15` (lower bound
raised here).

### 4. Sandbox teardown off the rollout's critical path

`env.cleanup()` ran in the rollout Ray task's `finally` block, so a
finished result only became fetchable after the sandbox DELETE returned
— and a teardown failure re-raised *over* the successful eval, degrading
it to a reward-0 error row. Teardown is also effectively one-shot
(`Sandbox.stop` latches `_closed` before dispatch), so a single failed
DELETE leaked the sandbox while still costing the rollout.

**Fix:** cleanup runs on a best-effort daemon thread; results return
immediately, teardown errors are logged rather than raised, and orphans
remain covered by the provider's sandbox TTL.

### 5. High-concurrency rollout delivery (`mini_swe_agent_2`)

Two coupled fixes so every finished rollout is delivered at high
concurrency (note: at very high concurrency the single-process
policy-model proxy remains a separate bottleneck — connection-refused
storms confirmed by a controlled A/B — with its scaling fix tracked
separately):

- **await the Ray ObjectRef** instead of `asyncio.to_thread(ray.get,
...)`: the default executor caps at `min(32, cpu+4)` threads, each
pinned for a full rollout, so delivery stalls at ~32 concurrent rollouts
and finished tasks queue behind blocked `ray.get` calls.
- **bounded litellm retries (`num_retries=5`, config-overridable)** — no
retry means one transient LLM-call failure kills a whole rollout;
unbounded retries make failures look like hangs — and
**`num_cpus=0.25`** on the rollout Ray task so concurrency is not capped
at cluster core count (rollouts are I/O-bound).

### 6. `datasets` declared in the base agent config

The struct-mode config merge rejects keys absent from the base config,
so a benchmark config using `_inherit_from` could not add its dataset
list (`ConfigKeyError: Key 'datasets' is not in struct`). Declared empty
in the base server config, matching how other agents (e.g. `swe_agents`)
expose it.

## Validation

- Requests/limits split (paired multi-node runs, identical except the
variable under test): infra-failed rollouts 41/316 (**13.0%**) at 2Gi
requests=limits → 19/288 (**6.6%**) at 8Gi limits / 2Gi requests; pass@1
58.0% → **67.1%**.
- Keepalive bound + health-checked create: zero `Server disconnected`
events and zero create-race 502s across all subsequent runs (previously
2–12 per run).
- Full stack, end-to-end from this branch: single-pass SWE-bench
Verified (500 instances, concurrency 500+) delivered **>98% of
rollouts** with ~1.6% residual sandbox-infra failures (server-side proxy
502s on established sandboxes, tracked separately) and zero
client-transport failures. Before the delivery fixes (NVIDIA-NeMo#4/NVIDIA-NeMo#5), an
identically shaped run lost the majority of *completed* evaluations at
the wall — hundreds of finished evals, only tens recorded.

## Relationship to NVIDIA-NeMo#2020

Complementary, no overlap; NVIDIA-NeMo#2020 (job attribution metadata) has since
merged and this branch is updated on top of it. Field-validated together
— the attribution labels are what make post-cancellation sandbox garbage
collection safely scoped to a single job.

## Testing

- `tests/unit_tests/test_opensandbox_provider.py`: transport-backend
coverage (httpx default with keepalive expiry, custom pool settings,
fallback when `httpx_aiohttp` is unavailable, `null` disables
injection), aiohttp opt-in (importorskip-guarded), and requests/limits
plumbing tests. 16 passed with and without `httpx-aiohttp`.
- `responses_api_agents/mini_swe_agent_2/tests/test_app.py`: updated for
the awaited ObjectRef (awaitable `FakeObjectRef`); assertions
strengthened to check the Ray call's params.
- CI green except a pre-existing `tau2` failure on `main` (reproduces on
unrelated PRs).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants