Repository navigation
Bound + cache workspace directory scans so large workspaces can't wedge agent boot - #8
Merged
Conversation
…n't wedge boot
Large AD-Workbench workspaces (many files on a slow Azure Files / blobfuse
mount) froze agent bootstrap and crash-looped the pod ("Agent could not
start" / "could not reach server"). Root cause: on_chat_start walked the
user-data mount ~4x via unbounded os.walk (the max_items caps only limited
the preview, not the traversal), and _build_user_data_sidebar_elements ran
synchronously on the asyncio event loop — starving the /healthz liveness
probe until Kubernetes restarted the pod. Fine with few files; fatal at scale.
Introduce biomni/fs_scan.py: one bounded, cached, thread-safe directory
scanner, and route every workspace walk through it.
- Bounded by BOTH a file-count cap (BIOMNI_WORKSPACE_MAX_FILES, default
10000) and a wall-clock deadline (BIOMNI_WORKSPACE_SCAN_TIMEOUT_S, default
15s) — a scan can never hang regardless of size or mount latency.
- TTL-cached (BIOMNI_WORKSPACE_SCAN_TTL_S, default 60s) behind a per-key lock
so the several inventory/sidebar builders of one chat start traverse the
tree once, not ~4x.
- Sidebar build moved off the event loop into run_in_executor and guarded, so
a slow/failed scan degrades to no panel instead of wedging the server.
- chainlit inventory/sidebar/overview/welcome and agent _get_data_root_items /
_get_user_data_resources / _get_data_lake_items all go through the scanner;
no raw unbounded os.walk on a user/network mount remains on the boot path.
- Honest UX: bounded scans render counts as a floor ("N+", "listing bounded"),
a timed-out scan reads "listing unavailable" (not "no files found"), and the
agent is told to enumerate on demand.
- runs/ excluded from scans (matches biomni.artifact); non-positive env values
fall back to defaults (the guard cannot be disabled).
Tests: tests/test_fs_scan.py (cap/deadline bounds, cache identity, depth +
exclusion pruning, env fallbacks). Verified end-to-end in docker: a 50k-file
workspace boots with /healthz=200, 0 restarts, scan capped at 10k in ~28ms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merged
10 tasks done
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.
Problem
Rolling out to prod, large AD-Workbench workspaces freeze agent bootstrap — the agent never starts ("Agent could not start" / "could not reach server"), with no useful logs. It works with a few files; it's fatal at scale.
Root cause
on_chat_startbuilds a file inventory for the sidebar + agent system prompt. On a large workspace on a slow network mount (Azure Files / blobfuse — everystatis a round-trip) this was catastrophic:os.walk; themax_items"preview" caps only limited what was displayed, so the walk stillstat-ed every file to compute totals._build_user_data_sidebar_elements()ran synchronously inon_chat_start, blocking the asyncio event loop for the whole walk →/healthzcould not be served → liveness (period 20s × failureThreshold 3) failed → Kubernetes crash-looped the pod.Fix
New
biomni/fs_scan.py: one bounded, cached, thread-safe directory scanner; every workspace walk routes through it.BIOMNI_WORKSPACE_MAX_FILES, default 10000) and a wall-clock deadline (BIOMNI_WORKSPACE_SCAN_TIMEOUT_S, default 15s) — a scan can never hang.BIOMNI_WORKSPACE_SCAN_TTL_S, default 60s) behind a per-key lock → the ~4 builders of one chat start traverse the tree once.run_in_executorand is guarded (a scan hiccup degrades to no panel, never a dead session).cl.Textconstruction in the worker is safe —run_in_executorcopies the Chainlit ws context and replays it viactx.run._get_data_root_items/_get_user_data_resources/_get_data_lake_itemsall go through the scanner — no raw unboundedos.walkon a user/network path remains on the boot path.10000+, "listing bounded"); a timed-out scan reads "listing unavailable" (not "no files found"); the agent is told to enumerate on demand.Ops can tune the three env knobs without a redeploy (e.g. raise caps for a large-but-fast mount). The anti-hang guarantee holds regardless.
Verification
Docker (real container, 50k-file workspace mounted read-only):
/healthzRestarts=0,Health=healthy(crash-loop gone)truncated, 28 mstimed_outon_chat_startfnsruns// hidden / excluded / depth pruningTests:
tests/test_fs_scan.py— cap & deadline bounds, cache identity, depth + exclusion pruning, env fallbacks (16 cases). Full suite collects clean (224).Code review: high-effort multi-agent review; findings addressed in this PR — bounded
_get_data_lake_items(a parallel hang vector on the boot path), timed-out-vs-empty messaging,runs/exclusion, non-positive env fallback, and consistent floor rendering across all UI surfaces.Note on base branch
Targets
feat/adworkbench(the repo default), notbiomni-ad: this change importsbiomni.observability/biomni.healthand edits post-divergence code that exist only onfeat/adworkbench(biomni-ad is 58 commits behind and lacks them). Happy to retarget if the intended flow differs.Follow-ups (out of scope)
biomni/agent/ad1.pystill has rawos.walk/os.listdirin the legacy Gradio path (not the Chainlit prod path) — route throughfs_scanin a follow-up.🤖 Generated with Claude Code