Commit 59ff31e
authored
feat: hibernate idle sessions, and a runtime built for coding agents (#76)
* feat: hibernate idle sessions instead of closing them
At the ceiling, sandboxd closed the least recently used idle session:
its container went, and so did its token, its event log and the
caller's ability to come back to it. Eviction now gives up only the
sandbox. The record stays, `GET /sessions` reports it as
`state: "hibernated"`, and the next request wakes it where it left off.
That splits one number into two, which is the point. `max_sessions`
now bounds resident sandboxes -- the RAM number -- while the new
`max_open_sessions` bounds sessions that exist at all, resident and
hibernated together, which is a disk number and properly much larger.
On a 4 GB host that is ten resident against a couple of hundred open.
Also here:
- `memswap_limit` on DockerSandbox, SandboxRuntime and SandboxdConfig.
Swap stays pinned to `mem_limit` by default, which is right when
swap is a disk; raising it is for hosts backed by zram, where the
pages stay in RAM compressed and the alternative is an OOM kill.
- scripts/bench_density.py, which measures on the host being sized
what no blog post can: marginal MemAvailable per session, per
container management overhead, time to first command, and wake
latency from hibernation.
- The reasoning, in docs/plans/sandbox-density-on-small-hosts.md,
including a correction to the earlier runtime-alternatives note:
microVMs buy isolation, not density, and lose badly under a hard
memory ceiling.
Three things this uncovered on the way. `sweep_workspaces` would have
deleted a hibernated session's workspace, since its keep-set came from
the manager alone. `SessionManager.release` returns False for a session
it does not hold, so `on_release` never fires and `close_session` has
to drop the records itself. And a purge of a hibernated session left
its persisted container behind, there being no sandbox object to stop.
* feat: a runtime built for coding agents, and two bugs it exposed
Measured rather than reasoned about, on Docker 29.2.1. Running the
sandbox the way an agent uses it turned up two things that were broken
before any tuning, both hitting the tool a coding agent reaches for
most.
Every orphaned process was a permanent zombie. Containers run
`sleep infinity` as PID 1, and `sleep` never calls `wait()`, so a
backgrounded server or anything the command timeout killed was
reparented to it and stayed. Measured: ten orphans, ten zombies, and
the process table growing 4 -> 13. They accumulate against
`pids_limit` until every command in the session fails to fork. Docker's
`init` reaps them for 488 kB.
git refused to work in the workspace. `workspace_root` bind-mounts a
host directory the container does not own, so every git command failed
with "detected dubious ownership" -- status, diff, log, commit alike --
and past that a commit failed again with "Author identity unknown". An
agent asked to save its work could not. Configured now through
`GIT_CONFIG_*` on the container rather than in an image, which is what
makes it reach the ready-made runtimes that build nothing of their own.
The runtime itself, `coding`, is 99.7 MB and eleven seconds to build.
`git` is 33.1 MB of that and unavoidable; ripgrep, fd, jq, less and
procps come to 4.3 MB between them, which is not a saving worth making
-- without `rg` an agent falls back to `grep -r` and floods its own
context, and without `procps` it has no way to see or stop the server
it just started. `build-essential` is refused at 94 MB to compile
wheels manylinux already ships built, and the base stays Debian slim
because on musl a package without a musllinux wheel builds from source.
`uv` is there because an agent installs packages inside somebody's
turn: 8485 ms -> 1169 ms for pandas. The surprise was at the ceiling --
uncapped, uv is OOM-killed by a 128 MB limit that pip survives, because
its parallelism is memory. Capped at two it fits, and is still 6.6x
faster than pip.
Also here: `polyglot` gets npm 10, since Debian ships a current Node
with an npm a major version behind; and `default_runtime` now takes the
first entry in `runtimes` rather than the literal alias "python", which
had forced every custom allowlist to contain that key.
Reasoning and the full measurements in docs/plans/coding-agent-runtime.md.1 parent ca4a8eb commit 59ff31e
16 files changed
Lines changed: 1839 additions & 91 deletions
File tree
- docs
- concepts
- plans
- scripts
- src/pydantic_ai_backends
- backends/docker
- remote
- ui
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
10 | 29 | | |
11 | 30 | | |
12 | 31 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| 67 | + | |
| 68 | + | |
67 | 69 | | |
68 | 70 | | |
69 | 71 | | |
| |||
82 | 84 | | |
83 | 85 | | |
84 | 86 | | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
85 | 118 | | |
86 | 119 | | |
87 | 120 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
235 | | - | |
| 235 | + | |
236 | 236 | | |
237 | 237 | | |
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
241 | 241 | | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
242 | 247 | | |
243 | 248 | | |
244 | 249 | | |
| |||
275 | 280 | | |
276 | 281 | | |
277 | 282 | | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
278 | 322 | | |
279 | 323 | | |
280 | 324 | | |
| |||
362 | 406 | | |
363 | 407 | | |
364 | 408 | | |
365 | | - | |
| 409 | + | |
366 | 410 | | |
367 | 411 | | |
368 | 412 | | |
369 | 413 | | |
370 | | - | |
| 414 | + | |
| 415 | + | |
371 | 416 | | |
372 | 417 | | |
373 | 418 | | |
374 | 419 | | |
375 | 420 | | |
376 | 421 | | |
377 | | - | |
378 | | - | |
379 | | - | |
380 | | - | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
381 | 444 | | |
382 | 445 | | |
383 | 446 | | |
384 | 447 | | |
385 | 448 | | |
386 | 449 | | |
387 | 450 | | |
388 | | - | |
389 | | - | |
390 | | - | |
391 | | - | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
392 | 460 | | |
393 | 461 | | |
394 | 462 | | |
| |||
504 | 572 | | |
505 | 573 | | |
506 | 574 | | |
507 | | - | |
508 | | - | |
509 | | - | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
510 | 580 | | |
511 | 581 | | |
512 | | - | |
| 582 | + | |
| 583 | + | |
513 | 584 | | |
514 | 585 | | |
515 | 586 | | |
| |||
0 commit comments