Skip to content

Fleet scaling: surface queue depth + capacity so more container instances spin up when work is queued#1617

Draft
pulzzejaehoon wants to merge 6 commits into
mainfrom
goal/7bf9a4a1
Draft

Fleet scaling: surface queue depth + capacity so more container instances spin up when work is queued#1617
pulzzejaehoon wants to merge 6 commits into
mainfrom
goal/7bf9a4a1

Conversation

@pulzzejaehoon

Copy link
Copy Markdown
Contributor

Integration PR for this goal — its tasks commit onto this branch (one PR per goal). Opened automatically by the engine on first push; left as a draft until the goal completes.

pulzzejaehoon and others added 6 commits June 30, 2026 03:47
…v1/me/fleet/capacity)

Expose a claim-faithful, autoscaler-pollable signal of engine queue depth vs
idle container-instance capacity so an operator (or external autoscaler) can
decide whether to spin up more daemon instances. Read-only/derivation only — no
machine-local lease coordination, no in-daemon concurrency state (server
surfaces depth + declares policy; daemon coordinates claims).

- src/lib/engine/fleet-capacity.ts: pure deriveFleetCapacity(queueDepth,
  machines) → { queueDepth, online, busy, available, shouldScale,
  recommendedInstances }, plus an async getFleetCapacity that sources depth from
  listFleetGoalsOrdered (the SAME fleetOwnerId-scoped, occupied-excluded order
  the claim path consumes — counts only 'queued' rows) and busy from live leased
  runs (isRunLive). shouldScale = queueDepth > available is the server-declared
  policy, kept in the pure module.
- Extract the daemon working/idle/stuck/error/offline decision tree from the
  fleet-dots route into classifyDaemonMachine (fleet.ts), so the capacity
  busy/idle counts and the fleet-dots strip cannot drift (faithful-scope).
- GET /api/v1/me/fleet/capacity via authorizeDaemonOrSession (autoscaler token
  or browser session), optional ?projectIds=a,b — mirrors the fleet-queue route.
- Unit tests for the pure derivation (zero machines, all busy, depth 0,
  depth>available, capacity>=depth, offline excluded); validated via node
  simulation since tsc/vitest can't run in this env.

Agent-config: additive read-only HTTP endpoint, not an agent tool — no new
domain entity/status/workflow/specialist. No PM-agent config change needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e:patch heap

Address review rework on the fleet-capacity goal.

Blocker (system:patch-coverage-below-threshold) — the patch-coverage gate runs
`npm run coverage:patch` and then checks only that coverage/lcov.info EXISTS
(container-loop ignores the command's exit code). The report was missing because
the full v8-instrumented suite (16080 tests) OOM'd at V8's old-space HEAP limit
before vitest could write the report — a heap ceiling, not a physical-RAM limit
(per the env-typecheck-incomplete finding), so the review runner has the RAM, the
run just needed a higher ceiling. Fixes:

- package.json: prefix coverage:patch with NODE_OPTIONS=--max-old-space-size=6144
  so the instrumented run reaches the report-writing step. Raising the ceiling
  cannot increase a run's actual memory or break a passing run — it only lets a
  heap-bound run finish (monotonically safe); matches the repo's inline-env script
  style (build/dev:ee).
- Add the missing unit tests so that once a report IS produced, PATCH coverage on
  the changed lines clears the 80% floor. Verified via a scoped lcov run:
  43/43 changed-instrumented lines covered = 100%.
    * fleet-capacity.test.ts: exercise the async getFleetCapacity assembler —
      QUEUED-only depth counting, a live-leased machine as busy (with the
      isRunLive expired-lease + null-machineId re-filters), the zero-machines
      run-query short-circuit, and a recently-failed-but-online machine counted
      as available. Mocks @/lib/db + ./fleet-queue; keeps ./fleet REAL so the
      shared classifyDaemonMachine/isRunLive predicates are exercised.
    * me/fleet/capacity/route.test.ts: 401 path, default all-projects call, and
      the comma-separated projectIds parse — mirrors the green fleet-queue route
      test (mocks authorizeDaemonOrSession + getFleetCapacity).

The refactored classifyDaemonMachine branches + the fleet-dots route map are
already covered by the existing machines/fleet route test (all five statuses).

Agent-config: still no PM-agent change — these are tests + a build-script tweak,
no agent-visible tool/data/workflow change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… coverage gates

Address review rework on the fleet-capacity goal — the two system blockers
(system:test-suite-failed and system:patch-coverage-below-threshold) share a
single root cause: timing-sensitive suites timing out under the full ~16k-test
run, not anything in the fleet-capacity change.

The 3 intermittent failures (agent-tools-route beforeEach dynamic-import hook,
authorization-fuzzing route import, engine-goals-client jsdom render) all pass
in isolation in well under a second each, but under the constrained review
container the 50%-worker pool oversubscribes the box and they crossed the old
15s test / default 10s hook ceilings. A timed-out test fails `npm run test`
(exit 1) AND aborts the v8-instrumented coverage run before coverage/lcov.info
is written — which is exactly the pair of gates that came back red.

Fix: raise testTimeout 15s→30s and set hookTimeout 30s (was the 10s default).
Raising a timeout only lets a slow run finish; it cannot turn a passing test
red, so this is monotonically safe. Verified locally: `npm run coverage:patch`
now exits 0, writes coverage/lcov.info (1.1MB), and the full suite reports
16086 passed / 0 failed (87.53% lines, above the 80% floor).

No fleet-capacity source change; no PM-agent config change (test-config only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Poll GET /api/v1/me/fleet/capacity (30s cadence + visibility/realtime
handling, alongside the existing fleet-dots/machines/queue polls) and
consume the shared FleetCapacity signal.

- Add an explicit capacity summary line: "N queued · M idle / K online".
- When shouldScale, render an under-provisioned prompt card (styled like
  the existing setup cards) wired to the /settings/machines Add-machine
  path, using recommendedInstances in the copy when > 0.
- Mirror the pressure as a small amber indicator in the collapsed strip.
- Read-only; no claim/lease changes. Gated so an idle fleet or a
  zero-machine state never shows the scale prompt (the existing
  "No machines connected" card owns that case).

No agent config update needed: read-only operator UI surface, adds no
tools and changes no agent-visible domain data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ect widget

Extend the per-project Fleet Activity glance beyond live runs to also show
queue depth and idle fleet capacity, reusing the fleet-capacity signal's
shared definitions so the two surfaces can never drift.

- fleet.ts: extend FleetActivityData with `queueDepth` (this project's
  runnable-but-unclaimed goals via `listRunnableGoalsWithKeys` — fleetOwnerId
  scope + occupied-goal exclusion, NOT a new query, bounded by
  FLEET_QUEUE_DEPTH_CAP) and `machines.busy`/`available`. getProjectFleet now
  derives capacity through new shared helpers `countMachineCapacity` +
  `loadDaemonMachineSignals`, kept as the single serializer (route + server
  render stay byte-identical).
- fleet-capacity.ts: refactor deriveFleetCapacity/getFleetCapacity onto the
  same shared helpers so busy/idle has one definition (faithful-scope).
- GET .../engine/fleet: forwards the extended payload (no logic change).
- fleet-activity-widget.tsx: header now reads
  "N running · M queued · online/total (K idle)" with a subtle "under
  capacity" link to /settings/machines when queueDepth > available. The
  primary scale prompt stays in the Fleet sidebar.

Read-only; no claim/lease changes. Adds widget + helper tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…overage gates

The full-suite gate (npm run test) and the patch-coverage gate both aborted on
the constrained 8-core review box: 16055/16057 tests passed, but 2 test files
died with 'Failed to start forks worker' / 'Timeout waiting for worker to
respond' — vitest's hardcoded 60s worker START_TIMEOUT exceeded because a
freshly forked worker could not get scheduled through its heavy module-import
phase while the other forks saturated the box. Any such abort fails the run and
leaves no coverage/lcov.info, blocking the coverage gate too. None of the
failing files are touched by this goal.

Even at 50% (4 forks here) the spawn window starved. Trimming to 40% (3 forks on
an 8-core box) widens the margin so a fresh fork can initialize in time, while a
proportional cap still scales up on larger CI hosts. Scheduling only — it cannot
turn a passing test red, only let a slow run finish cleanly and emit lcov.info.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

1 participant