Skip to content

feat(api,runner): surface sandbox file-descriptor exhaustion as degradedReason - #5002

Open
mu-hashmi wants to merge 13 commits into
mainfrom
feat/fd-exhaustion-degraded-state
Open

feat(api,runner): surface sandbox file-descriptor exhaustion as degradedReason#5002
mu-hashmi wants to merge 13 commits into
mainfrom
feat/fd-exhaustion-degraded-state

Conversation

@mu-hashmi

@mu-hashmi mu-hashmi commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

TL;DR: sandboxes whose host-side filesystem path exhausts file handles now expose a nullable degradedReason (set by the runner, cleared on recovery, readable via API/clients/TS SDK) - surfacing only, with a dedicated test guaranteeing it can never trigger automated recovery.

Description

When the host-side fd budget under a sandbox is exhausted, everything inside breaks at once (dynamic loader failures, exec errors) while the sandbox still reports started; users debug their own code while the platform is the cause. This PR names that condition end to end:

API

  • sandbox.degradedReason (nullable text, pre-deploy migration) + an entity invariant clearing it on any transition away from STARTED (the invariant runs on every entity update write).
  • PUT /sandboxes/:sandboxId/degraded-reason for runners, mirroring the updateSandboxState guard stack. Setting a non-empty reason on a non-STARTED sandbox returns 409 so the runner knows the push did not land and retries (a silent ignore could permanently suppress the flag after a startup race); clears stay idempotent. DTO requires non-empty when non-null.

Runner

  • Classifier requires canonical fd-exhaustion forms: "too many open files" (case-insensitive), errno-24 variants, or EMFILE:/ENFILE: immediately followed by the phrase. Bare EMFILE/ENFILE tokens do NOT classify (filename-shaped text like /tmp/EMFILE.txt: no such file must never brick-flag a sandbox). Content-type gate is case-insensitive.
  • Detection on the toolbox proxy's modifyResponse hook with hard size caps (8KB for >=400, 64KB for 200 /process/execute), io.LimitReader with consumed-byte restoration, and - on a failed body read - replay of the consumed prefix followed by the original read error, so the client observes the same failure. The hook can never fail the proxied request.
  • SandboxDegradedService: pushes the reason on first sighting (the pushing claim is taken synchronously before the goroutine spawns), 30s probe loop (daemon exec of true) clears on recovery, 30m stale expiry force-clears when probes are indeterminate, restart re-seeds from the API with retry-until-success (capped backoff, context-aware). Probe-clear and stale-expiry both skip while a push is in flight; inspect errors distinguish not-found (drop) from transient (keep probing).
  • The foot-gun guard: fd-exhaustion signatures are deliberately excluded from recoverableErrorPatterns; recovery_test.go asserts every signature maps to UnknownRecoveryType / !IsRecoverable. This can never trigger restart/recreate.

SDK: TypeScript Sandbox mirrors degradedReason (dashboard reads the regenerated client directly). Python/Go/Ruby wrapper parity is a deliberate follow-up.

Design tradeoffs: detection is traffic-or-probe driven (onset can lag until something touches the sandbox); 30m stale expiry favors self-healing over a stuck flag. Rollout: runner pushes against an older API 404 and are treated as not-pushed (retried), so deploy order is safe in both directions.

Review guide

Core (~20 hand-written files) vs generated (35+ client files across 6 libs; guarded by the CI regen-drift gate - skim):

  1. apps/runner/pkg/services/sandbox_degraded.go - THE risk center: per-sandbox state machine, push/clear/expiry serialization.
  2. apps/api/src/sandbox/services/sandbox.service.ts + entities/sandbox.entity.ts - 409 contract + clearing invariant.
  3. apps/runner/pkg/common/degraded.go + recovery.go - classifier + the non-recoverability guarantee.
  4. apps/runner/pkg/api/controllers/proxy.go - sniffing caps and error replay.
  5. TS SDK mirror + tests.

Commit map

Commit What Why it exists
2fc1067 api: field, migration, endpoint implementation (plan)
06e7eb9 client regen for the field implementation (generated)
d7288b0 classify fd exhaustion as non-recoverable implementation (plan)
a5cf9c4 proxy detection + degraded service implementation (plan)
28bbac3 TS SDK mirror review follow-up (acceptance criterion)
aef5b23 push/expiry race guards, inspect classification, LimitReader Copilot review (4 valid)
170c519 DTO rejects empty reason Copilot review (valid)
ef563c3 claim-before-spawn, probe single-inspect, seed retry, errno word-boundary cubic review (valid)
f42bc34 409 on non-started + runner retry cubic review P1 (valid)
f6e02af canonical-form matching, seed retry-until-success cubic review (valid)
9077843 client regen for the 409 response map CI drift gate (local regen had a stale nx cache)
8259633 restore tracked .env reverts a regen-tooling deletion from the previous commit; net diff zero across the pair
d6e6572 replay sniffed read errors; case-insensitive content type cubic review (2 valid, late wave)

Related PRs

Independent of the snapshot family. Siblings: #5001 (fd telemetry), #5003 (daemon shell resolution).

Validation

  • API: entity-invariant spec (clear on non-started / keep on started), controller auth-coverage entry, DTO spec, 409 spec; full nx test api green twice (52 suites).
  • Runner: go test ./pkg/services/ ./pkg/common/ ./pkg/api/controllers/ in a linux container, incl. dedicated race tests (claim observable immediately; clear-vs-push interleavings) and -race runs; classifier table tests incl. four filename false-positive shapes and uppercase content type; read-error replay regression test.
  • CI mirror: yarn generate:api-client idempotent on the committed regen; yarn lint:fix + yarn format (incl. format:py under the poetry venv) zero drift; go work sync clean.
  • All CI checks green on d6e6572 (incl. E2E).

Closes #4996

…endpoint

Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
…ce degradedReason

Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
@nx-cloud

nx-cloud Bot commented Jun 11, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit d6e6572

Command Status Duration Result
nx run-many --target=build --all --parallel=6 -... ✅ Succeeded 1m View ↗
nx run-many --target=test --all --nxBail=true ✅ Succeeded 1m 4s View ↗
nx run-many --target=docs --all ✅ Succeeded <1s View ↗
nx run-many --target=format --all --parallel=6 ✅ Succeeded 3s View ↗
nx run api:lint ✅ Succeeded <1s View ↗
nx run-many --target=generate:api-client --all ... ✅ Succeeded 9s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-11 20:08:53 UTC

@mu-hashmi
mu-hashmi marked this pull request as ready for review June 11, 2026 03:32
Copilot AI review requested due to automatic review settings June 11, 2026 03:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new, additive “degraded” signal on sandboxes to surface host-side file-descriptor exhaustion while the sandbox remains STARTED. It adds API persistence + runner-driven reporting/clearing logic, and regenerates SDK/API clients to expose the new field and runner endpoint.

Changes:

  • API: add nullable sandbox.degradedReason, DB migration, invariant clearing on non-STARTED, and a runner-authenticated PUT /sandbox/:sandboxId/degraded-reason.
  • Runner: detect fd-exhaustion signatures from toolbox proxy responses, push the degraded reason once, and probe periodically to clear (with stale expiry); add explicit guard tests ensuring fd-exhaustion never triggers automated recovery.
  • SDK/clients/docs: expose degradedReason across generated clients + TS SDK model/docs and add round-trip tests.

Reviewed changes

Copilot reviewed 53 out of 56 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
libs/sdk-typescript/src/Sandbox.ts Adds degradedReason to TS SDK Sandbox model.
libs/sdk-typescript/src/tests/Sandbox.test.ts Tests DTO round-trip for degradedReason.
libs/api-client/src/models/update-sandbox-degraded-reason-dto.ts Adds TS API-client DTO for update endpoint.
libs/api-client/src/models/sandbox.ts Adds degradedReason to TS API-client Sandbox model.
libs/api-client/src/models/index.ts Re-exports new TS DTO model.
libs/api-client/src/api/sandbox-api.ts Adds TS API-client method for degraded-reason endpoint.
libs/api-client/src/.openapi-generator/FILES Tracks new generated TS model file.
libs/api-client-ruby/lib/daytona_api_client/models/update_sandbox_degraded_reason_dto.rb Adds Ruby DTO model for update endpoint.
libs/api-client-ruby/lib/daytona_api_client/models/sandbox.rb Adds degraded_reason to Ruby Sandbox model.
libs/api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb Adds Ruby client call for degraded-reason endpoint.
libs/api-client-ruby/lib/daytona_api_client.rb Requires new Ruby DTO model.
libs/api-client-ruby/.openapi-generator/FILES Tracks new generated Ruby model file.
libs/api-client-python/daytona_api_client/models/update_sandbox_degraded_reason_dto.py Adds Python DTO model for update endpoint.
libs/api-client-python/daytona_api_client/models/sandbox.py Adds degraded_reason to Python Sandbox model.
libs/api-client-python/daytona_api_client/models/init.py Exports new Python DTO model.
libs/api-client-python/daytona_api_client/api/sandbox_api.py Adds Python client call for degraded-reason endpoint.
libs/api-client-python/daytona_api_client/init.py Exposes new Python DTO model at package root.
libs/api-client-python/.openapi-generator/FILES Tracks new generated Python model file.
libs/api-client-python-async/daytona_api_client_async/models/update_sandbox_degraded_reason_dto.py Adds async Python DTO model for update endpoint.
libs/api-client-python-async/daytona_api_client_async/models/sandbox.py Adds degraded_reason to async Python Sandbox model.
libs/api-client-python-async/daytona_api_client_async/models/init.py Exports new async Python DTO model.
libs/api-client-python-async/daytona_api_client_async/api/sandbox_api.py Adds async Python client call for degraded-reason endpoint.
libs/api-client-python-async/daytona_api_client_async/init.py Exposes new async Python DTO at package root.
libs/api-client-python-async/.openapi-generator/FILES Tracks new generated async Python model file.
libs/api-client-java/src/test/java/io/daytona/api/client/model/UpdateSandboxDegradedReasonDtoTest.java Adds generated Java model test stub.
libs/api-client-java/src/test/java/io/daytona/api/client/model/SandboxTest.java Adds generated Java Sandbox test stub for field.
libs/api-client-java/src/test/java/io/daytona/api/client/api/SandboxApiTest.java Adds generated Java API test stub for endpoint.
libs/api-client-java/src/main/java/io/daytona/api/client/model/UpdateSandboxDegradedReasonDto.java Adds generated Java DTO model.
libs/api-client-java/src/main/java/io/daytona/api/client/model/Sandbox.java Adds degradedReason to generated Java Sandbox.
libs/api-client-java/src/main/java/io/daytona/api/client/JSON.java Registers new Java DTO type adapter factory.
libs/api-client-java/src/main/java/io/daytona/api/client/api/SandboxApi.java Adds generated Java API call for degraded-reason endpoint.
libs/api-client-java/.openapi-generator/FILES Tracks new generated Java files.
libs/api-client-go/model_update_sandbox_degraded_reason_dto.go Adds Go DTO model for update endpoint.
libs/api-client-go/model_sandbox.go Adds DegradedReason field to Go Sandbox model.
libs/api-client-go/api/openapi.yaml Adds endpoint + schema updates to OpenAPI spec.
libs/api-client-go/api_sandbox.go Adds Go client method for degraded-reason endpoint.
libs/api-client-go/.openapi-generator/FILES Tracks new generated Go model file.
apps/runner/pkg/services/sandbox_degraded.go Implements runner degraded tracking, push, probe, clear.
apps/runner/pkg/services/sandbox_degraded_test.go Tests pushInFlight behavior.
apps/runner/pkg/runner/runner.go Wires SandboxDegradedService into runner singleton.
apps/runner/pkg/docker/daemon_exec_probe.go Adds daemon exec probe helper for recovery/clear checks.
apps/runner/pkg/common/recovery.go Documents fd-exhaustion exclusion from recoverable patterns.
apps/runner/pkg/common/recovery_test.go Asserts fd-exhaustion never maps to recoverable actions.
apps/runner/pkg/common/degraded.go Adds fd-exhaustion signature matching + classifier.
apps/runner/pkg/common/degraded_test.go Unit tests for matcher/classifier and truncation.
apps/runner/pkg/api/controllers/proxy.go Adds response sniffing hook for fd-exhaustion detection.
apps/runner/cmd/runner/main.go Initializes and starts degraded tracking service.
apps/docs/src/content/docs/en/typescript-sdk/sandbox.mdx Documents degradedReason in TS SDK docs.
apps/api/src/sandbox/services/sandbox.service.ts Adds service method to update degraded reason.
apps/api/src/sandbox/entities/sandbox.entity.ts Adds DB column + invariant clearing on state changes.
apps/api/src/sandbox/entities/sandbox.entity.spec.ts Tests entity invariant behavior for degradedReason.
apps/api/src/sandbox/dto/update-sandbox-degraded-reason.dto.ts Adds DTO for degraded-reason update payload.
apps/api/src/sandbox/dto/sandbox.dto.ts Adds degradedReason to API Sandbox DTO mapping.
apps/api/src/sandbox/controllers/sandbox.controller.ts Adds runner endpoint PUT :sandboxId/degraded-reason.
apps/api/src/sandbox/controllers/sandbox.controller.auth.spec.ts Adds auth-coverage test entry for new endpoint.
apps/api/src/migrations/pre-deploy/1781135992705-migration.ts Adds pre-deploy migration for degradedReason column.
Files not reviewed (3)
  • libs/api-client-go/api_sandbox.go: Language not supported
  • libs/api-client-go/model_sandbox.go: Language not supported
  • libs/api-client-go/model_update_sandbox_degraded_reason_dto.go: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +234 to +258
s.mu.Lock()
entry, ok := s.entries[sandboxId]
if !ok {
s.mu.Unlock()
return
}
entry.reason = reason
entry.lastConfirmed = time.Now()
needsPush := !entry.reported && !entry.pushing
s.mu.Unlock()

if !needsPush {
return
}

if err := s.pushReason(sandboxId, reason); err != nil {
s.log.Warn("Failed to push degraded reason, will retry on next probe tick", "sandboxId", sandboxId, "error", err)
return
}

s.mu.Lock()
if entry, ok := s.entries[sandboxId]; ok {
entry.reported = true
}
s.mu.Unlock()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in aef5b23 (unified pushReasonGuarded: every reason push claims entry.pushing under the mutex) and hardened in ef563c3 (the claim happens synchronously before the goroutine spawns).

Comment on lines +271 to +279
s.mu.Lock()
entry, ok := s.entries[sandboxId]
stale := ok && time.Since(entry.lastConfirmed) > degradedStaleAfter
s.mu.Unlock()

if !stale {
return
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in aef5b23 — expiry clear is skipped while a push is in flight and retried next tick, same guard as the healthy-clear path.

Comment on lines +197 to +202
c, err := s.docker.ContainerInspect(ctx, sandboxId)
if err != nil || c == nil || c.State == nil || !c.State.Running {
s.drop(sandboxId)
s.log.DebugContext(ctx, "Dropped degraded tracking for non-running sandbox", "sandboxId", sandboxId)
return
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in aef5b23 — inspect errors are classified: not-found drops tracking, transient errors keep the entry probing (stale-expiry remains the backstop).

Comment on lines +94 to +107
body, err := io.ReadAll(resp.Body)
if err != nil {
// Partial read: hand back what was consumed plus the original body so
// the proxy surfaces the same read error to the client; skip
// classification.
resp.Body = &compositeReadCloser{
Reader: io.MultiReader(bytes.NewReader(body), resp.Body),
Closer: resp.Body,
}
return
}
resp.Body.Close()
resp.Body = io.NopCloser(bytes.NewReader(body))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in aef5b23 — io.LimitReader(bodyCap+1) with consumed-byte restoration; over-cap reads skip classification. The failed-read path now also replays the original error (d6e6572).

Comment thread apps/api/src/sandbox/dto/update-sandbox-degraded-reason.dto.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

7 issues found across 56 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/runner/pkg/docker/daemon_exec_probe.go">

<violation number="1" location="apps/runner/pkg/docker/daemon_exec_probe.go:38">
P2: This probe redundantly calls `ContainerInspect`, doubling Docker API work in the degraded polling loop.</violation>
</file>

<file name="apps/runner/cmd/runner/main.go">

<violation number="1" location="apps/runner/cmd/runner/main.go:210">
P2: Starting the degraded tracker without retrying the initial API seed can strand pre-existing degradedReason flags after runner restart.</violation>
</file>

<file name="apps/api/src/sandbox/entities/sandbox.entity.ts">

<violation number="1" location="apps/api/src/sandbox/entities/sandbox.entity.ts:454">
P2: This invariant misses pending transitions away from STARTED, so degradedReason stays stale during stop/archive/destroy flows.</violation>
</file>

<file name="apps/api/src/sandbox/services/sandbox.service.ts">

<violation number="1" location="apps/api/src/sandbox/services/sandbox.service.ts:3115">
P1: Silently ignoring non-STARTED degraded-reason updates can permanently suppress the degraded flag after a startup race.</violation>
</file>

<file name="apps/runner/pkg/common/degraded.go">

<violation number="1" location="apps/runner/pkg/common/degraded.go:38">
P2: Bare `EMFILE`/`ENFILE` substring checks can falsely mark healthy sandboxes degraded when toolbox errors echo user-controlled paths.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread apps/api/src/sandbox/services/sandbox.service.ts
Comment thread apps/runner/pkg/services/sandbox_degraded.go Outdated
Comment thread apps/runner/pkg/docker/daemon_exec_probe.go Outdated
Logger: logger,
Docker: dockerClient,
})
sandboxDegradedService.Start(ctx)

@cubic-dev-ai cubic-dev-ai Bot Jun 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Starting the degraded tracker without retrying the initial API seed can strand pre-existing degradedReason flags after runner restart.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/runner/cmd/runner/main.go, line 210:

<comment>Starting the degraded tracker without retrying the initial API seed can strand pre-existing degradedReason flags after runner restart.</comment>

<file context>
@@ -202,6 +202,13 @@ func run() int {
+		Logger: logger,
+		Docker: dockerClient,
+	})
+	sandboxDegradedService.Start(ctx)
+
 	// Initialize SSH Gateway if enabled
</file context>
Fix with cubic

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in ef563c3 (bounded retry) and finished in f6e02af — seeding retries until success with capped backoff, context-aware.

Comment on lines +454 to +456
if (this.state !== SandboxState.STARTED && this.degradedReason != null) {
changes.degradedReason = null
}

@cubic-dev-ai cubic-dev-ai Bot Jun 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This invariant misses pending transitions away from STARTED, so degradedReason stays stale during stop/archive/destroy flows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/sandbox/entities/sandbox.entity.ts, line 454:

<comment>This invariant misses pending transitions away from STARTED, so degradedReason stays stale during stop/archive/destroy flows.</comment>

<file context>
@@ -448,6 +451,10 @@ export class Sandbox {
       changes.recoverable = true
     }
 
+    if (this.state !== SandboxState.STARTED && this.degradedReason != null) {
+      changes.degradedReason = null
+    }
</file context>
Suggested change
if (this.state !== SandboxState.STARTED && this.degradedReason != null) {
changes.degradedReason = null
}
if (
(this.state !== SandboxState.STARTED || this.desiredState !== SandboxDesiredState.STARTED) &&
this.degradedReason != null
) {
changes.degradedReason = null
}
Fix with cubic

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewed, not changed: the invariant runs on every entity update write, and stop/archive/destroy flows write real state transitions through it. While state==STARTED with only desiredState pending, the sandbox is still running and the flag is still accurate; it clears on the actual transition.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Got it — thanks for the clarification.

Comment thread apps/runner/pkg/common/degraded.go Outdated
Comment thread apps/api/src/sandbox/dto/update-sandbox-degraded-reason.dto.ts
Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/runner/cmd/runner/main.go">

<violation number="1" location="apps/runner/cmd/runner/main.go:210">
P2: Starting the degraded tracker without retrying the initial API seed can strand pre-existing degradedReason flags after runner restart.</violation>
</file>

<file name="apps/api/src/sandbox/entities/sandbox.entity.ts">

<violation number="1" location="apps/api/src/sandbox/entities/sandbox.entity.ts:454">
P2: This invariant misses pending transitions away from STARTED, so degradedReason stays stale during stop/archive/destroy flows.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/runner/pkg/services/sandbox_degraded.go Outdated
…tching

Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
…dbox

Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
Copilot AI review requested due to automatic review settings June 11, 2026 04:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 55 out of 58 changed files in this pull request and generated no new comments.

Files not reviewed (3)
  • libs/api-client-go/api_sandbox.go: Language not supported
  • libs/api-client-go/model_sandbox.go: Language not supported
  • libs/api-client-go/model_update_sandbox_degraded_reason_dto.go: Language not supported

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/runner/cmd/runner/main.go">

<violation number="1" location="apps/runner/cmd/runner/main.go:210">
P2: Starting the degraded tracker without retrying the initial API seed can strand pre-existing degradedReason flags after runner restart.</violation>
</file>

<file name="apps/api/src/sandbox/entities/sandbox.entity.ts">

<violation number="1" location="apps/api/src/sandbox/entities/sandbox.entity.ts:454">
P2: This invariant misses pending transitions away from STARTED, so degradedReason stays stale during stop/archive/destroy flows.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread apps/runner/pkg/common/degraded.go Outdated
Comment thread apps/runner/pkg/services/sandbox_degraded.go Outdated
…ntil success

Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>
Copilot AI review requested due to automatic review settings June 11, 2026 04:58
Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 55 out of 58 changed files in this pull request and generated 2 comments.

Files not reviewed (3)
  • libs/api-client-go/api_sandbox.go: Language not supported
  • libs/api-client-go/model_sandbox.go: Language not supported
  • libs/api-client-go/model_update_sandbox_degraded_reason_dto.go: Language not supported

Comment on lines +97 to +105
body, err := io.ReadAll(io.LimitReader(resp.Body, bodyCap+1))
if err != nil {
// Partial read: hand back what was consumed plus the original body so
// the proxy surfaces the same read error to the client; skip
// classification.
resp.Body = &compositeReadCloser{
Reader: io.MultiReader(bytes.NewReader(body), resp.Body),
Closer: resp.Body,
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d6e6572 — the consumed prefix is replayed followed by the stored read error itself, so the client observes the same failure instead of a silent truncation. Regression test reads the replayed body and asserts the original error.

Comment thread apps/runner/pkg/common/degraded.go
…nsensitively

Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu>

@rajpratham1 rajpratham1 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice feature addition. This introduces a clear way to surface sandbox file-descriptor exhaustion without conflating it with recoverable failures.

I like that the implementation spans the full stack consistently: the API, runner, persistence layer, SDKs, and generated clients all stay in sync. The detection logic is intentionally conservative, avoids false positives, and explicitly keeps fd-exhaustion separate from the automatic recovery pipeline. The proxy buffering safeguards, retry behavior, invariant enforcement, and extensive regression tests also make the feature much more robust.

The scope is large, but the changes appear cohesive, well-tested, and aligned around a single capability. I don't see any blocking issues. Nice work.

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.

runner: surface sandbox file-descriptor exhaustion as a degraded state

3 participants