Repository navigation
Events carry the grant facts, and capacity and queue depth become events #345
Copy link
Copy link
Closed
Labels
task:readySpec checked. An agent may implement it once its Depends on allow.Spec checked. An agent may implement it once its Depends on allow.
Description
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 onlease.grantedandlease.rejected; newcapacity.changedandqueue.changed.src/core/lease-lifecycle.ts—granttakes 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 togrant; maps it tosource: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.rejectedemitter addsrequestIdandrequester: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.tsandsrc/gateway/fleet-coordinator.tsmint the request id before the admission checks, so a request refused askilledoralready-leasedhas an id although nolease.requestedwas 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 platformrunning,maxRunning,reserved,warm;ramBudget { usedBytes, limitBytes }when the strategy keeps one);status.getinsrc/daemon/dispatcher.tscalls 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 emitscapacity.changedwhen 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.tsand the gateway's fleet queue insrc/gateway/fleet-coordinator.ts— emitqueue.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.mdand the row indocs/internal/adr/README.md— the status becomes Accepted — not yet implemented: the feature has leftfeature:spec, and this is the first PR under it.
Contract and event changes
lease.grantedpayload:+ requestId: string,+ source: "warm" | "booted" | "provisioned". Additive.lease.rejectedpayload:+ 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.tsdoes 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.mdrules 3 and 6: post-commit emission; additive payloads.architecture.mdrule 10: the plan-kind-to-source map exists once.testing.mdrule 4: the test that everylease.rejectedcarriesrequestIdandrequestercovers every emitter, not one.
Tests
- a lease granted from a ready device emits
lease.grantedwithsource: "warm"and the request's id - a lease granted after booting a shutdown device emits
lease.grantedwithsource: "booted" - a lease granted on a device provisioned for it emits
lease.grantedwithsource: "provisioned" - a lease granted after an eviction carries the source of the plan that followed it:
provisionedwhen a device was created,bootedwhen a shut-down device was booted - every emitter of
lease.rejectedincludes the request id and the requester: one test that iterates the reason union inEventMapand has a case for each, so a reason added without a case fails it; the worker'stimeout,daemon-restarted,killed,already-leased,no-wait,boot-timeout,unresolvable-specandcancelled, and the gateway'stimeout,already-leased,cancelled,unresolvable-spec,no-workerandno-wait - a request refused as
already-leasedcarries the same request id the admitted request would have had, and nolease.requestedprecedes it status.getand the capacity observer build their figures through the same functioncapacity.changedis emitted after a provision commits, with the figuresstatus.getreports at that momentcapacity.changedis emitted when a provisioning reservation is taken and when it is releasedcapacity.changedandqueue.changedare emitted once when the daemon has startedcapacity.changedis emitted after a device is deleted and after a lease releases a warm devicecapacity.changedis not emitted when a commit leaves the capacity figures unchangedcapacity.changedcarriesramBudgetunder the resource strategy and omits it under the fixed strategyqueue.changedis emitted when a request joins the worker's queue and when it leaves, with the new depth- a gateway emits
queue.changedfor its fleet queue and never relays a worker'squeue.changedas its own - (e2e)
simlock events --since 1hafter one lease showslease.grantedwithrequestIdmatching the precedinglease.requested
Done when
- After
simlock lease --platform iosagainst the fake driver,simlock events --since 1hshows alease.grantedwhoserequestIdequals thelease.requestedbefore it and whosesourceisprovisioned; a second lease on the same device after release showswarm. - After a
--no-waitrequest is refused, itslease.rejectedcarries the request id and the requester; after a second request from a requester that already holds a lease, thealready-leasedrejection carries a request id and nolease.requestedprecedes it. simlock events --since 1hshows acapacity.changedat daemon start and one per change of the capacity figures, aqueue.changedat start and one per change of the queue depth, and no two consecutive events of either kind with equal payloads.- Both
EVENTS.mdfiles 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.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
task:readySpec checked. An agent may implement it once its Depends on allow.Spec checked. An agent may implement it once its Depends on allow.
Activity
github-actions commentedon Oct 5, 2026
Now
task:ready: approved, technical spec present.feat(events): events carry the grant facts; capacity and queue depth …
chore: replace the branch's earlier history with the rebased commit (#…
test: the warm grant is on the same device as the first grant (#345)
V3RON commentedon Oct 5, 2026
Handoff
Done: PR #351 (ready, branch
task/345) implements the spec:lease.grantedcarriesrequestIdandsource,lease.rejectedcarriesrequestIdandrequesteron every reason,capacity.changedandqueue.changedexist 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:214and:232: the events view no longer shows adaemon.startedrow. 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.
test: the Events view e2e reads daemon.started where the virtualized …