OXYGENHUB
# 8aa45fa6ece5 oxygenflow / docs / handover.md
12.6 KiB · Markdown · last changed The WASM tier: WASI modules run by wasmtime on any runner (closes #75) 10 days ago renderedsource blame history raw
39e737fb Andy McCall ↶ 1
# Handover: jobs that outlive their runner
2
3
Proposed 2026-10-25, from a design conversation with Andy. Issues:
4
oxygenflow#75 (WASM tier), #76 (leases and drain), #77 (checkpoints),
5
#78 (hedging), #79 (trust classes), #80 (browser runners); the phone
6
runner is oxygenhub-android#8. Builds on Jetstream (docs/jetstream.md),
7
whose pools (hosts, Kubernetes, cloud) and requeue of a lost runner's job
8
this extends.
9
10
## 1. Why
11
12
Three situations, one problem: a job is tied to the runner that started
13
it.
14
15
- **The runner goes away.** A phone leaves the office Wi-Fi or comes off
16
  charge; a laptop sleeps; a spot VM is reclaimed with 30 seconds' notice;
17
  a Kubernetes node is drained. Today the job starts again from nothing
18
  elsewhere, or fails.
19
- **A better runner frees up.** A job went to the small runner B because
20
  the big runner A was busy. A finishes its own job when B is 10% through,
21
  then sits idle while B grinds through the other 90%.
22
- **The same work runs twice.** A requeued job redoes everything; two
23
  pipelines that build the same thing both build it.
24
25
Solving the first makes unreliable runners usable (phones, browsers,
26
spare machines, spot instances). Solving the second keeps a mixed fleet
27
busy. Together they are the case a company makes for running CI on the
28
hardware it already has: more use from it, and less waiting for
29
developers and agents.
30
31
## 2. What Flow already has
32
33
Most of the parts exist:
34
35
- **Jobs are the packets.** A job runs one `exec`; jobs form a graph and
36
  pass artifacts (§5 of the spec, oxygenflow#58). A run is already spread
37
  over runners: oxygenhub's run #439 built on earth-1, ran clippy on
38
  plex-1, dist on plex-1 and riscv64 on narvi-1.
39
- **Receipts already chain.** Each job's receipt (spec §6.1) is signed by
40
  its runner and names its inputs, which are other jobs' outputs, whose
41
  receipts name *their* runners. A run's provenance is already a graph of
42
  signed receipts from several runners.
43
- **A lost runner costs only its job.** Jetstream requeues it after a
44
  grace, and a spot reclaim notice requeues it at once (jetstream.md §11).
45
- **Placement can wait for a better runner** ("held for a better
46
  runner"), and the hub learns each job's duration per speed class
47
  (spec §11).
48
49
What is missing: a job that is *partly done* is lost when it moves; there
50
is no graceful way to leave; a better runner cannot take over; nothing
51
says how far to trust a runner; and phones and browsers cannot run jobs
52
at all (no WASM tier).
53
54
## 3. Decision: move progress, not processes
55
56
**No live migration of running VMs** (oxygenflow#33 suggested Firecracker
57
snapshots). A snapshot fixes the VM's shape: a job started with 2 vCPUs
58
on a small runner is restored with 2 vCPUs on the big one, since
59
Firecracker cannot add vCPUs to a running VM, so it gets no faster. It
60
moves gigabytes of memory, works only between hosts of the same
61
architecture and CPU features, and cannot help when the runner simply
62
disappears, because the snapshot is on the runner that is gone.
63
64
Instead a job's **progress** moves: the outputs of finished jobs (as
65
today) and, within a job, the directories it declares as checkpoints
66
(§7). Everything is content-addressed, so the new runner fetches only what
67
it lacks, and every piece comes with a receipt saying who made it.
68
69
## 4. Runners
70
71
| Kind | Tiers | Typical trust (§9) | Leaves by |
72
|---|---|---|---|
73
| Hosts (earth-1, plex-1, mars-1, narvi-1) | microvm, syscall, wasm | owned | restart or update (drain) |
74
| Kubernetes pods | microvm (KVM nodes), syscall, wasm | owned or attested | node drain, eviction, scale-down (SIGTERM) |
75
| Cloud VMs, spot or on demand | microvm, syscall, wasm | attested | reclaim notice, scale-down |
76
| Phone, the easy way (oxygenhub-android#8) | wasm | device | off charge, off the allowed network, too hot, owner stops it |
77
| Phone, full Linux (oxygenhub-android#9, later) | syscall in Android's Linux VM | device | as above |
78
| Browser tab (#80) | wasm | anonymous, or device when a member signs in | tab closed or hidden |
79
80
Cloud and Kubernetes are where handover pays most: spot capacity costs
81
60 to 90% less, and handover is what makes it safe to run CI on it.
82
Phones and browsers are where it shows best in a demonstration.
83
84
**Architecture still matters.** Native outputs and checkpoints only move
85
between runners of the same image and target (x86_64 to x86_64, arm64 to
86
arm64). WASM work moves anywhere.
87
88
## 5. The WASM tier (oxygenflow#75)
89
28cc603f Andy McCall ↶ 90
Spec §4.1 names it; built 2026-10-25 (wasmtime 49, WASI preview 1).
39e737fb Andy McCall ↶ 91
92
- wasmtime, WASI preview 1 first (preview 2 components later).
93
- A virtual file system holding the job's scope and inputs; outputs
94
  collected from it as for the other tiers.
95
- No network. Memory from the sandbox's limit; CPU time bounded by fuel
96
  and the job's time limit.
97
- The receipt's environment names the runtime, its version and the
98
  module's hash.
99
- Every Linux runner offers it (`tiers` gains `wasm`); phones and
100
  browsers offer only it.
101
102
What runs there is what compiles to WASM: linters, formatters, policy
103
checks, generated docs, and **test suites built for `wasm32-wasip1`**.
104
That last one makes the first demonstration (§11): a native job builds
105
the test modules, a phone runs them.
106
107
## 6. Leases and drain (oxygenflow#76)
108
109
**Lease.** A runner holding a job holds a lease, renewed by its logs and
110
a heartbeat. When the lease lapses the job is requeued, as today.
111
112
**Drain** is one message in the runner contract (jetstream.md §4) for
113
every *planned* departure:
114
115
| Trigger | Notice |
116
|---|---|
117
| SIGTERM, `flow` restart or update | the runner's grace (75 min on Earth) |
118
| Kubernetes node drain or eviction | the pod's grace period |
119
| Spot reclaim | 30 s (GCP), 2 min (AWS) |
120
| Phone off charge, off the allowed network, too hot | the owner's policy (a minute or so) |
121
| Browser tab hidden or closing | a few seconds |
122
123
On drain the runner decides from the job's learned duration (spec §11):
124
125
1. the rest is shorter than the notice: finish the job, then leave;
126
2. otherwise: save its checkpoints (§7), upload them, release the lease
127
   with `drained`, and stop the job.
128
129
A drained job is requeued at once, **ahead of new work**, and resumes
130
from its checkpoints. A runner that drains a job is not penalised; one
131
that drops leases without draining loses placement priority.
132
133
## 7. Checkpoints (oxygenflow#77)
134
135
A job may declare directories that hold its progress:
136
137
```kdl
138
job "build" {
139
    sandbox "rust"
140
    exec "cargo" "build" "--release"
141
    checkpoint "/src/target"
142
}
143
```
144
145
- **Saved** by content (FastCDC chunks, deduplicated, kept for the run's
146
  life) on drain and every few minutes while it runs (a sandbox setting,
147
  default 5 minutes, only while the job has run at least that long).
148
- **Restored** before a resumed job runs the **same exec again**. Tools
149
  that skip finished work (cargo's fingerprints, gradle, make, ninja)
150
  then carry on where they stopped; a tool that does not just starts
151
  again, so it is never wrong, only slower.
152
- **Receipts.** The partial run ends in a receipt with `exit` "drained",
153
  naming the checkpoint's hash as an output and signed by its runner. The
154
  resumed job's receipt names that checkpoint among its inputs. So
155
  provenance says that plex built the first part and mars finished it,
156
  and both are signed.
157
- **Cheap to move inside a cloud:** checkpoints live in the run's store
158
  (a bucket under Strata for cloud pools), so a new pod in the same region
159
  restores them quickly. From a phone, the policy (charger, office
160
  network) keeps uploads cheap.
161
- **Checkpoints are code.** A tampered `.rlib` in `target/` ends up in
162
  the binary. So a job accepts a checkpoint only from a runner of at
163
  least its trust class (§9), and its receipt says that it used one.
164
165
## 8. Hedging (oxygenflow#78)
166
167
When a markedly faster runner frees up while a job runs on a slow one,
168
and the learned durations say it would finish sooner (counting the time
169
to restore the latest checkpoint), start the job there too. The first to
170
finish wins; the other is cancelled.
171
172
- **Bounded:** one hedge per job, and only where a policy allows it (the
173
  critical path of a run, or always on owned runners). Never for jobs
174
  with side effects (deployments, publishing).
175
- **No tricky handover:** the slow runner is never interrupted. If the
176
  fast one fails, nothing is lost.
177
- **A free determinism check:** when both finish (a race), their outputs
178
  are compared; a difference is reported as a non-hermetic job.
179
- **Cost-aware in clouds:** starting a bigger instance costs money, so a
180
  pool may forbid hedging, or allow it only for jobs a person is waiting
181
  on.
182
183
Hedging plus holding briefly for a better runner (which exists) gets most
184
of the benefit of a full handover, for a fraction of the work. Build it
185
before anything finer (§12).
186
187
## 9. Trust classes (oxygenflow#79)
188
189
How far to believe a receipt depends on who signed it:
190
191
| Class | Who | Key |
192
|---|---|---|
193
| `attested` | cloud confidential VMs, vTPM-measured hosts | keyless workload identity (oxygenhub#122) |
194
| `owned` | the organisation's registered hosts | the runner's identity, or keyless |
195
| `device` | members' phones and laptops | the device's Keystore key, under the member's grant |
196
| `anonymous` | browsers not signed in | a key held non-extractable by WebCrypto |
197
198
- A sandbox or job may require a minimum (`trust "owned"`). Release and
199
  publish jobs default to `owned`.
200
- A job resumes only from checkpoints made at its class or above.
201
- Receipts and provenance record the class of every runner that
202
  contributed. The trust page shows a run's weakest link.
203
- **Spot checks:** a sample of jobs run by `device` and `anonymous`
204
  runners is run again elsewhere and the outputs compared (with
205
  oxygenhub#298's independent rebuilds). A runner whose results disagree
206
  is held for its owner to look at.
207
208
## 10. Phones and browsers
209
210
**Phones** (oxygenhub-android#8): one tap registers the phone as a
211
WASM-tier runner, as a foreground service, scoped like any runner. The
212
owner's policy says when it may run: on charge, on these networks (the
213
office Wi-Fi), under a temperature, within a storage budget. Leaving
214
those conditions drains it (§6). The runner itself is Flow's runner code
215
built for Android through the app's Rust core (rust-core, uniffi), with
216
wasmtime for the tier. The full Linux runner in Android's Linux terminal
217
(#9) comes later.
218
219
**Browsers** (#80): a page on the hub turns a tab into a WASM-tier runner
220
in a Web Worker: WASI through a shim, OPFS (the browser's private file
221
storage) for files, uploads as for any runner. Limits: about 4 GB of
222
memory, threads only on a cross-origin-isolated page, throttled or
223
stopped in a background tab (a drain). This is mostly a demonstration of
224
handover and spot checks on the least reliable runner there is, not
225
capacity.
226
227
## 11. The first demonstration
228
229
A pipeline over oxy-core:
230
231
1. `build-wasm-tests` (microvm, any x86_64 host): `cargo test
232
   --target wasm32-wasip1 --no-run`, outputs the test modules.
233
2. `test-wasm` (wasm, `runs-on "android"`, depends on 1): runs each
234
   module under the WASM tier, on the phone.
235
236
The script: start the run; the phone takes `test-wasm`; switch the
237
phone's Wi-Fi off halfway (a drain under its policy); the job goes back
238
to the queue and plex-1 takes it in its WASM tier; the run page shows
239
which runner did what, with the receipts and their trust classes. Then
240
the same with a pipeline that has a checkpoint, to show a job resuming
241
rather than restarting.
242
243
## 12. Order of work
244
245
1. **WASM tier** (#75), in `flow` on Linux runners.
246
2. **Phone runner** (oxygenhub-android#8) on that tier, with its policy.
247
3. **Leases and drain** (#76), with requeue ahead of new work.
248
4. The demonstration (§11), job-level: drain and requeue.
249
5. **Checkpoints** (#77): resume within a job; receipts that say so.
250
6. **Trust classes** (#79), before anything other than our own runners
251
   joins.
252
7. **Hedging** (#78), then measure on our own CI: queue and run times
253
   with and without it.
254
8. **Browser runners** (#80).
255
9. **Finer pieces,** only if the measurements call for them: test
256
   sharding and per-crate builds (#29), so that a long job is many short
257
   ones a better runner can take over between.
258
259
Cloud and Kubernetes pools come through Jetstream (jetstream.md §9,
260
oxygenflow#33), and get drain and checkpoints as they arrive.
261
262
## 13. Open questions
263
264
- Checkpoint retention: the run's life, or kept as a cache for the next
265
  run of the same job (a remote cache, which would then need the same
266
  trust rules)?
267
- How long may a drained job wait for a runner of its trust class before
268
  it falls back to restarting from nothing on any runner?
269
- Hedging costs compute: a per-pool budget, or a per-organisation one?
270
- A phone's battery and heat: are the owner's policy limits enough, or
271
  should the runner also cap its own CPU use?