Skip to content

Events carry the grant facts, and capacity and queue depth become events #345

Closed

Description

@V3RON

Part of #329.

Scope

After this PR, the event history holds every fact the usage figures need. simlock events shows on each lease.granted the request it served and whether the device was warm, booted or provisioned; each lease.rejected names its request and requester; and capacity.changed and queue.changed appear whenever a worker's capacity or a queue's depth changes. ADR 0016 §2 and §3 fix the shapes.

Technical spec

Modules touched

  • src/bus/index.ts — EventMap: new fields on lease.granted and lease.rejected; new capacity.changed and queue.changed.
  • src/core/lease-lifecycle.ts — grant takes the request id and the plan's source and emits them.
  • src/core/lease-acquisition-coordinator.ts — passes the waiter's request id and the kind of the plan that grants to grant; maps it to source: grant-ready → warm, boot-shutdown → booted, provision → provisioned. An eviction (evict-running, evict-managed) never grants by itself: the coordinator plans again after it, and that plan's kind is the source. The map lives here and nowhere else.
  • Every lease.rejected emitter adds requestId and requester: src/core/lease-engine.ts (timeout), src/core/startup-converger.ts (daemon-restarted), src/core/lease-acquisition-coordinator.ts (killed, already-leased, no-wait, boot-timeout), src/gateway/fleet-coordinator.ts (timeout, already-leased, cancelled, unresolvable-spec, no-worker).
  • src/core/lease-acquisition-coordinator.ts and src/gateway/fleet-coordinator.ts mint the request id before the admission checks, so a request refused as killed or already-leased has an id although no lease.requested was emitted for it. The id minted there is the one the stored request gets when admission passes; nothing mints a second.
  • src/core/capacity/ — one function builds the capacity figures from a registry snapshot and the coordinator (per platform running, maxRunning, reserved, warm; ramBudget { usedBytes, limitBytes } when the strategy keeps one); status.get in src/daemon/dispatcher.ts calls it instead of building its own. A new observer in the same module calls it after every registry commit and after every reservation taken or released, and emits capacity.changed when the result differs from the last emitted. It emits once at startup, after convergence, so every run begins with a step. Two consecutive emissions never carry equal figures.
  • src/core/wait-queue.ts and the gateway's fleet queue in src/gateway/fleet-coordinator.ts — emit queue.changed { depth } when the depth changes, and once at startup with the depth then.
  • docs/EVENTS.md, docs/internal/EVENTS.md — both catalogs.
  • docs/internal/adr/0016-usage-figures-are-derived-on-read-from-the-event-history.md and the row in docs/internal/adr/README.md — the status becomes Accepted — not yet implemented: the feature has left feature:spec, and this is the first PR under it.

Contract and event changes

  • lease.granted payload: + requestId: string, + source: "warm" | "booted" | "provisioned". Additive.
  • lease.rejected payload: + requestId: string, + requester: string, both required on every reason. Additive.
  • New capacity.changed: { ios: { running, maxRunning, reserved, warm }, android: { … }, global: { running, maxRunning, reserved, warm }, ramBudget?: { usedBytes, limitBytes } }. Emitter: the capacity module. Worker only.
  • New queue.changed: { depth: number }. Emitters: the worker's wait queue, the gateway's fleet queue.
  • The gateway relays payloads without parsing these fields, so the protocol range in src/contract/protocol.ts does not move. If the implementer finds the relay does parse them, it moves per ADR 0003 §6 and the PR body says so.

Rules in play

  • events.md rules 3 and 6: post-commit emission; additive payloads.
  • architecture.md rule 10: the plan-kind-to-source map exists once.
  • testing.md rule 4: the test that every lease.rejected carries requestId and requester covers every emitter, not one.

Tests

  • a lease granted from a ready device emits lease.granted with source: "warm" and the request's id
  • a lease granted after booting a shutdown device emits lease.granted with source: "booted"
  • a lease granted on a device provisioned for it emits lease.granted with source: "provisioned"
  • a lease granted after an eviction carries the source of the plan that followed it: provisioned when a device was created, booted when a shut-down device was booted
  • every emitter of lease.rejected includes the request id and the requester: one test that iterates the reason union in EventMap and has a case for each, so a reason added without a case fails it; the worker's timeout, daemon-restarted, killed, already-leased, no-wait, boot-timeout, unresolvable-spec and cancelled, and the gateway's timeout, already-leased, cancelled, unresolvable-spec, no-worker and no-wait
  • a request refused as already-leased carries the same request id the admitted request would have had, and no lease.requested precedes it
  • status.get and the capacity observer build their figures through the same function
  • capacity.changed is emitted after a provision commits, with the figures status.get reports at that moment
  • capacity.changed is emitted when a provisioning reservation is taken and when it is released
  • capacity.changed and queue.changed are emitted once when the daemon has started
  • capacity.changed is emitted after a device is deleted and after a lease releases a warm device
  • capacity.changed is not emitted when a commit leaves the capacity figures unchanged
  • capacity.changed carries ramBudget under the resource strategy and omits it under the fixed strategy
  • queue.changed is emitted when a request joins the worker's queue and when it leaves, with the new depth
  • a gateway emits queue.changed for its fleet queue and never relays a worker's queue.changed as its own
  • (e2e) simlock events --since 1h after one lease shows lease.granted with requestId matching the preceding lease.requested

Done when

  • After simlock lease --platform ios against the fake driver, simlock events --since 1h shows a lease.granted whose requestId equals the lease.requested before it and whose source is provisioned; a second lease on the same device after release shows warm.
  • After a --no-wait request is refused, its lease.rejected carries the request id and the requester; after a second request from a requester that already holds a lease, the already-leased rejection carries a request id and no lease.requested precedes it.
  • simlock events --since 1h shows a capacity.changed at daemon start and one per change of the capacity figures, a queue.changed at start and one per change of the queue depth, and no two consecutive events of either kind with equal payloads.
  • Both EVENTS.md files list the two new events and the two changed payloads.

Out of scope

  • Reading the events back as figures (later tasks).
  • Changing how long the event file is kept (next task).

Depends on

Approval

  • Approved for delivery

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 5, 2026
  2. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 5, 2026
  3. github-actions commented on Oct 5, 2026

    @github-actions

    Now task:ready: approved, technical spec present.

  4. self-assigned this
    on Oct 5, 2026
  5. V3RON commented on Oct 5, 2026

    @V3RON
    ContributorAuthor

    Handoff

    Done: PR #351 (ready, branch task/345) implements the spec: lease.granted carries requestId and source, lease.rejected carries requestId and requester on every reason, capacity.changed and queue.changed exist with one shared figures function, both catalogs updated, ADR 0016 moved to Accepted — not yet implemented. Two review rounds: one finding fixed, five rejected with reasons. Mutate: 119 mutants, 0 alive. Review notes posted on the PR.

    Not done: the merge gate refused it: not merged: CI not green: fail Console.

    Findings: the Console lane fails in chromium and firefox on e2e/console/events.spec.ts:214 and :232: the events view no longer shows a daemon.started row. The two new events now fill the feed the view reads, so the row the tests look for is pushed out or filtered. Either the view's history window or the tests' expectation needs to follow the new event volume.

    Blocked on: nothing. One fix run for the console lane, then the gate again.

    Written by an agent.

  6. assigned and unassigned on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions