Repository navigation
Description
[Frontier] [POOL15-01] Place work across several enrolled working computers in one team pool
Parent
S15 — #316. Canonical spec: docs/specs/S15-conversational-autonomy.md.
Model tier
Frontier: placement, work affinity, capacity fencing and lease/settlement semantics across several VMs are concurrency and isolation decisions; the resources UI must be authored by Claude Opus 5.5 at effort high.
Gap found (verified on main c5f476e)
The VM store and WorkingVmService.openAttempt already accept any registered vmId, and VmManagers holds up to 64 adapters. But the composed runtime uses exactly one VM: S15RuntimeConfig.vm has a single id; composeS15 passes options.vm.id to createConversationVmTools (src/s15/conversation/vm-port.ts uses the fixed vmId); githubRuntime receives config.vm.id; the computer card and managerId getter assume one VM. A second enrolled VM therefore can never receive work. The owner decision of 2026-11-03 requires the single-host nested VM as the default plus optional extra owner-supplied VMs for more compute, with sensible session placement.
Context to read
AGENTS.md; CONTEXT.md; docs/specs/S15-conversational-autonomy.md; docs/adr/0067; docs/contracts/s15-working-vm-v1.md; src/s15/vm/** (service.ts capacity/fences, store.ts, enrollment.ts, managers.ts); src/s15/team/vm.ts and team/service.ts (TEAM lease); src/s15/conversation/vm-port.ts; src/s15/github/runtime.ts; src/s15/composition.ts; src/s15/main.ts (S15RuntimeConfig.vm); src/s15/web/setup.ts and resources.ts; docs/design/S15-conversational-experience.md (F1, F7, Resources).
Deliverable
The team's working computer resource is a pool of one or more enrolled VMs. New work is placed on a ready VM with free capacity; all later sessions of the same work stay on the VM that holds its workspace; system-mode administration targets that VM only. The owner can add another computer (existing enrollment, optional Lima/Proxmox management) and drain or remove one from the Resources page.
Implementation steps
- Config and composition: replace the single
vm.idwith a pool (default member = the local nested VM from SETUP15-01 when present). Keep reading the existing single-VM config shape as a one-member pool so the staged arm64 controller and fixtures keep working. Fresh installs never need a hand-written pool list: members come from enrollment records in the protected store. - Placement: a protected placement function chooses the VM for a new work attempt: only
ready, unfenced VMs with free session capacity; prefer the VM already holding the work's workspace (work affinity, persisted); otherwise least loaded by active sessions over capacity, tie-break by the default member. If none has capacity, return the same wait/refusal behavior the single VM has today. Inspect access may read from the VM that holds the work. Placement never moves a work's files between VMs. - Leases and fences: TEAM leases, POLICY snapshot fences, stop proof and uncertainty fencing stay per VM. An uncertain or stopped VM receives no new work; work bound to it waits or reports the precise boundary.
- Consumers: conversation VM tools, GitHub runtime and any other
vmIdconsumer use placement or the work's affinity instead of a fixed id. - Resources UI (Claude Opus 5.5, effort high):
/resources/computerlists each computer with its status, capability rows and active sessions; "Add another computer" opens the existing enrollment flow; "Stop using this computer" drains it (no new placement; existing work finishes or reports). th and en keys for every new string. - Docs: update the working-VM contract and runbook for the pool, placement and drain semantics.
Acceptance criteria
- With two enrolled ready VMs, new work is placed on the less loaded one; later attempts of the same work return to the VM that holds its workspace.
- A fenced, uncertain, stopped or drained VM receives no new work; bound work reports the precise boundary.
- System-mode administration takes the exclusive lease and snapshot on the work's VM only; other VMs keep serving.
- The existing single-VM config shape keeps working unchanged.
- The Resources page lists every computer and supports add and drain, authored by Claude Opus 5.5 at effort high.
Tests and evidence
- Placement unit tests: two ready VMs with loads 1/2 and 0/2 → second; affinity overrides load; all full → existing capacity behavior; fenced/uncertain/drained excluded.
- Affinity persistence across controller restart.
- System mode on VM A while VM B keeps serving sessions.
- Conversation and GitHub runtime use placement (composition tests with two fake VMs).
- Single-VM legacy config → one-member pool; existing composition/runtime suites pass unchanged.
- Web rendering tests (th and en) for list, add and drain states.
Requirements
FR-02/05/16/21/35/36/38; NFR-07/08/14. Acceptance: S15-AC03, S15-AC04.
Blocked by
- [Frontier] [VM15-02] Run the working VM as a Jit-managed nested KVM guest on the controller host #366 VM15-02 (local manager kind). Bundled in one PR with SETUP15-01 ([Frontier] [SETUP15-01] Create the working computer automatically during S15 setup on KVM hosts #369).
Contracts used/delivered
Uses WorkingVmService, VmEnrollment, TEAM lease wrapper. Delivers a pool/placement API with TSDoc and the pool-aware runtime config reader. Do not rename existing exported APIs.
Out of scope
Moving workspaces between VMs; per-Bot restriction to a specific VM; heterogeneous architecture scheduling beyond refusing a mismatch the work explicitly requires; the production installer.
Do not
Do not let model output choose a VM id or endpoint; do not share fences/leases across VMs; do not evict running work when draining; do not add AI attribution; do not merge the PR yourself.
Definition of done
bun install --frozen-lockfile, bun run typecheck, bun run test, git diff --check pass on the final HEAD. PR body: Closes #370 and Closes #369, tested SHA, not-run limits. No merge by the implementer.
Activity
feat(s15): create the working computer automatically and place work a…