Skip to content

Reserve a device for one kind of work (growth phone guard) - #106

Merged
addisdev merged 15 commits into
developfrom
feat/reserved-device
Oct 23, 2026
Merged

addisdev merged 15 commits into
developfrom
feat/reserved-device

Conversation

@addisdev

Copy link
Copy Markdown
Owner

Why

The Galaxy S8+ (988a1b3541354f565a) is about to be cabled to ultra and kept for one job: driving the Instagram app for the growth pilot (taylab-launch-kit#229, ui-test jobs pinned with targets.match to that id, targets.executor: ultra-phone, max_attempts: 1). Once ultra runs an executor, any other Android host job could claim that signed-in phone and open other apps on it: an ad hoc os ~ 'android', a fan-out, the explore night, or a schedule without a pin. Pools and matches can't stop that, because they describe what a job wants. A reservation describes what the device accepts.

What

  • fleet device reserve <id> <label> [--note T] / fleet device unreserve <id>, backed by POST /api/v1/devices/{id}/reserve and /unreserve (admin scope). Reservations go in a new device_reservations table, not on the device row. That way a phone can be reserved before it is cabled up, and the reservation survives the registry forgetting the device.
  • The rule (collector/src/reserved.ts, pure). A reserved device goes only to a job that has both:
    • "reservation": "<label>", a new top-level job field
    • a pin on the device: targets.device_id, or a targets.match that pins its id. "Pins" is tested by behaviour: the expression must match the device and must stop matching when its id (and a serial equal to it) is swapped for another. device_id == 'X' && … pins; device_id == 'X' || os ~ 'android' does not.
  • Where it is enforced:
    • matchingDevices: fan-out and the composer preview
    • specEligibleFor: device-executor claims and preemption
    • executorsHolding: host routing, so an unpinned Android job no longer routes to ultra just because the reserved phone is attached there
    • the host executor's own selectTargets, which gets every reservation as reserved_devices in the /executor/next-job answer. This is a new optional protocol field, not stored on the job.
    • the web-shots Android-Chrome profile
  • At the door: a job that pins a reserved device without its label is refused with the new error code device_reserved (409). Validate warns left out as reserved for other work: <id> (<label>), and fan-out's no_matching_devices names the reserved devices it left out.
  • Visible: reserved_for on /api/v1/devices, a RESERVED column in fleet devices, reserved_for in fleet_devices, and fleet run --reservation LABEL. The skill tells agents to leave reserved devices alone.
  • Fix found on the way: the executor described an Android target without device_id. So the kit's device_id == '<serial>' pin was routed to the right executor and then matched nothing there (skip, "no targets"). The executor's descriptor now includes device_id.

For the growth phone (kit #229)

fleet device reserve 988a1b3541354f565a growth-instagram --note "Instagram growth pilot, signed in"

and each job spec adds one top-level field:

{ "reservation": "growth-instagram",
  "targets": { "executor": "ultra-phone", "match": "device_id == '988a1b3541354f565a'", "exclusive": true },
  "lease": { "max_attempts": 1 } }

Tests

  • cd collector && npm test: ALL PASS. The dashboard build ran too. New smoke section 41 covers reserve-before-register, conflicts, the shelf listing, fan-out with and without the label, validate warnings, device_reserved on both enqueue routes, host routing (the phone's executor skips the unpinned job and takes the growth job; the claim carries reserved_devices), a reserved device-executor agent, and unreserve. New src/reserved.test.ts covers the rule and the executor's selectTargets, including the device_id match fix.
  • cd fleet && npm test: 82 pass, 2 skipped (pre-existing). New CLI test for reserve, list, conflict, the refused pin, a run with --reservation, and unreserve; the MCP fleet_devices test now checks reserved_for.
  • mkdocs build --strict: clean.

Notes

🤖 Generated with Claude Code

addisdev and others added 14 commits September 20, 2026 09:00
Bumps  and [fast-uri](https://github.com/fastify/fast-uri). These dependencies needed to be updated together.

Updates `fast-uri` from 3.1.7 to 3.1.8
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.7...v3.1.8)

Updates `fast-uri` from 4.1.4 to 4.2.1
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.7...v3.1.8)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.8
  dependency-type: indirect
- dependency-name: fast-uri
  dependency-version: 4.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [fastify](https://github.com/fastify/fastify) from 5.12.1 to 5.12.5.
- [Release notes](https://github.com/fastify/fastify/releases)
- [Commits](fastify/fastify@v5.12.1...v5.12.5)

---
updated-dependencies:
- dependency-name: fastify
  dependency-version: 5.12.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](7rulnik/source-map-js@v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Installers copy skills/ from the release
A device reserved with `fleet device reserve <id> <label>` is offered only
to a job whose new `reservation` field is that label and that pins the
device by id (targets.device_id, or a targets.match such as
device_id == '<id>'). Fan-out, pools, unpinned matches, the composer's
preview, device-executor claims and host-executor routing pass it by, and
the host executor applies the same rule to the devices it can see, from the
`reserved_devices` map its claim now carries. A job that pins a reserved
device without the label is refused with device_reserved.

For the growth pilot's signed-in Instagram phone on ultra, which would
otherwise be open to any os ~ 'android' job, a fan-out or the explore night.

Reservations are a table in the brain's database (device_reservations), so a
phone can be reserved before it is cabled up and survives the registry
forgetting it. POST /api/v1/devices/{id}/reserve and /unreserve (admin);
reserved_for on /api/v1/devices, `fleet devices` and fleet_devices;
`fleet run --reservation`.

Also fixes the executor describing an Android target without device_id, so a
host job pinned by targets.match "device_id == '<serial>'" was routed to the
right executor and then matched nothing there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@addisdev
addisdev changed the base branch from main to develop October 23, 2026 02:01
@addisdev
addisdev merged commit fe091fb into develop Oct 23, 2026
24 checks passed
@addisdev
addisdev deleted the feat/reserved-device branch October 23, 2026 02:35
This was referenced Oct 23, 2026
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