Skip to content

Provider maintenance idle-shutdown timer is never scheduled when a maintenance request never settles #3230

Description

@dfaivre-pcs

Summary

Provider bridge workers spawned in ~/.bb/provider-maintenance-workspace accumulate and are never retired, despite PROVIDER_MAINTENANCE_IDLE_TIMEOUT_MS being 60 s. On a box that had been up ~13 h with 7–9 live threads, 7–8 of them were resident simultaneously, in cohorts up to ~12 minutes old, holding ~700–770 MB. They are npm version probes (npm view @openai/codex version, npm list @anthropic-ai/claude-code, npm list @earendil-works/pi-coding-agent), so each is a fresh ~85–136 MB node process that has no work left to do.

Verified live against bb-app 0.42.1, node v24.13.1, WSL2 (6.6.87.2), 4 installed providers (provider-acp, provider-claude-code, provider-codex, provider-pi).

This is distinct from #2308 and #2289. The thread-serving bridge workers are healthy on this build: startIdleProviderSessionReaper fires as designed, and the daemon log carries 17 Reaped idle provider sessions records with idleForMs of 1.85–2.09 M (31–35 min), matching IDLE_PROVIDER_SESSION_REAP_AFTER_MS. There is exactly one worker per live thread environment. Only the maintenance workspace accumulates.

Evidence

One snapshot, taken by reading /proc/<pid>/cwd and /proc/<pid>/cmdline (process titles are rewritten, so pgrep/ps cannot see any of this — pgrep -c node reports 0 on this box):

pid 30688   provider-acp               522s old    84 MB
pid 30689   provider-acp               522s old    85 MB
pid 30695   provider-acp               522s old    85 MB
pid 30790   provider-acp               522s old    86 MB
pid 8381    provider-claude-code       741s old   136 MB
pid 8383    provider-codex             741s old   110 MB
pid 8388    provider-acp               741s old    86 MB

All are children of the single host daemon pid, all with cwd = ~/.bb/provider-maintenance-workspace, all running bb-provider-bridge-worker.mjs.

The two cohorts are what makes this conclusive. They are 219 s apart in a single snapshot: the 741 s cohort was still alive when the 522 s cohort was spawned, and both are far past the 60 s idle timeout. Working retirement would have removed the older cohort long before the newer one existed. An earlier snapshot the same day showed 8 simultaneous workers in the same workspace at ages 370–460 s, spawned in bursts of three — consistent with mapProviderMaintenanceRequests' concurrency limit of 3.

provider-acp accumulates fastest: 5 of 8 workers in one snapshot, 4 of them in a single cohort, suggesting the ACP bridge spawns one per configured ACP agent and none is retired.

What the logs say

~/.bb/logs/host-daemon.20.log contains zero occurrences of either warning on the shutdown path:

  • Failed to shut down idle provider maintenance runtime — 0
  • Failed to shut down provider maintenance runtime during invalidation — 0

So shutdownProviderMaintenanceRuntime() is not failing loudly. Either it never runs, or it completes without terminating the worker processes.

Two candidate mechanisms

I did not narrow this further — offering both rather than asserting one:

  1. The idle timer is never scheduled. withProviderMaintenanceRuntime schedules it only in its finally, and only when providerMaintenanceActiveRequests reaches 0 (apps/host-daemon/src/runtime-manager.ts, scheduleProviderMaintenanceIdleShutdown). A probe request that never settles — npm view against a slow or unreachable registry has no timeout of its own — keeps the counter above 0 forever, so the 60 s timer is never armed and the workers outlive every subsequent probe round.
  2. The shutdown does not reach the workers. If the timer does fire, runtime.shutdown() resolving without terminating the spawned bb-provider-bridge-worker.mjs children would leave exactly this residue, silently, since nothing on that path logs success.

The cohort structure slightly favours (1): each round leaves its whole burst behind, rather than some workers dying and others surviving.

Suggested direction

Whatever the cause, the shape of the work looks over-provisioned for what it produces. A version probe is a short, cacheable, read-only question, and the current design pays a fresh ~100 MB node process per provider per round to ask it. Three things would each help independently:

  • Cache the answer across rounds with a TTL, so an idle box stops re-probing at all.
  • Bound the probe with its own timeout, so a hung npm view cannot pin providerMaintenanceActiveRequests above 0 indefinitely.
  • Log the shutdown, not only its failure. The silence on this path is why the accumulation is invisible until someone counts processes by /proc/<pid>/exe.

Impact

On a 48 GB dev box shared with Docker and a browser this was ~700 MB of the ~10 GB the whole agent stack held, and it is the component that grows with uptime rather than with thread count — so it is the one that gets worse the longer the machine stays up. bb-app stop clears it.

Workaround

bb-app stop and restart. There is no way to retire them without taking the daemon down.


Downstream tracking issue, with the census tooling used to find this: https://github.com/PremierCrop/pcs/issues/1341

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    hostHost daemon, process lifecycle, memory, event looppartial-reproBug partially reproduced; some claims unverified; see linked reportperfprovidersCross-provider bridges, models, login

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions