# M17–M18 execution plan: process lifecycle + device manager **Archived — completed 2026-07-13** (every item checked; suite ended 54/54). Kept as the record of how M17–M18 landed; the successor is [m19-m20-plan.md](m19-m20-plan.md). The operational plan for building [process-lifecycle.md](process-lifecycle.md) (M17) and [device-manager.md](device-manager.md) increments 5–7 (M18). Design is settled in those documents; this file is the build order — one phase at a time, each phase green before the next starts. Delete or archive this file when M18 lands. **Definition of green, every phase:** `zig build` clean, `zig build test` clean, `python3 test/qemu_test.py` passes (existing scenarios plus the phase's new one), and the relevant design doc's "known gaps" / status lines updated. Commit per green phase (no co-author trailers). **Workflow (settled 2026-07-12):** work happens in a dedicated git worktree, on feature branches cut from `main` — `feat/process-lifecycle` (M17.1–17.4), `feat/device-manager` (M18.1), `feat/usb-xhci-bus` (M18.2–18.3). When a branch's phases are all green it is **auto-merged into `main`**; branches are kept after merge, not deleted. Merges and branches are pushed to origin. Phase 0 (once): commit the design docs, merge the outstanding `feat/usb` work into `main`, and run the existing QEMU suite green before any new work starts. **Numbering note:** continues the milestone sequence (driver track ended at M16). ## Status The loop marks a phase `[x]` in the same commit that lands it. A phase is marked only when its definition of green holds. - [x] **Phase 0** — baseline: docs committed, feat/usb merged to main, pushed; `usb-xhci-libary.zig` renamed to `usb-xhci-library.zig`; existing QEMU suite green from the worktree (48/48, 2026-07-12). - [x] **M17.1** — kernel releases claims/MSI on death (claims: `releaseAllOwnedBy` in the reap; MSI was already swept by `irq.releaseOwner`; `claim-release` test; suite 49/49) - [x] **M17.2** — exit reasons (`ExitReason` recorded at exit/fault/kill before the notification; `process_exit_reason` supervisor-gated; `runtime.process.exitReason`; kernel + ring-3 assertions; suite 49/49) - [x] **M17.3** — published exit events + VFS subscriber (`process_subscribe`, bounded ref-counted table, publish on every death; `runtime.process.subscribeExits`; VFS handles carry owners and are swept on the owner's death; `vfs-client-death` test; suite 50/50) - [x] **M17.4** — signals, timer notifications, `runtime.process`, the service harness (signal_bind/process_signal + coalescing pending mask; timer_bind on the tick; bindSignals/signalsFrom/sendSignal/stop + timerOnce; runtime.service.run with the zero-length ping; VFS converted; `signals` scenario; suite 51/51) - [x] **merge** `feat/process-lifecycle` → main, push (merged 2026-07-13) - [x] **M18.1** — device-manager protocol: hello + restart policy (device-manager-protocol module; the manager as a harness service: supervised spawns, hello deadline via timer sweep, restart with 300/600/1200ms backoff, exit reasons deciding restart-vs-stopped, crash-loop cap; usb-xhci-bus first conforming driver; crash-test fixture re-proving claim release each respawn; `driver-restart` scenario; maximum_tasks 16→32 — the sweep was overflowing the pool; suite 52/52) - [x] **merge** `feat/device-manager` → main, push (merged 2026-07-13) - [x] **M18.2** — xHCI port scan + tree reports (child_added/child_removed in the protocol; the manager's child mirror with death-pruning; xHCI maps the register BAR — resource 0 is ECAM — reads CAPLENGTH/HCSPARAMS1, scans PORTSC, reports connected ports with speed-class identity; `usb-report` scenario proves report → prune → respawn → re-report; suite 53/53) - [x] **M18.3** — app surface: enumerate/subscribe over IPC (subscriber endpoint rides as the call's capability; events are the same structs the buses send); device-list first client; protocol capped at the kernel's IPC MESSAGE_MAXIMUM (256); the startUserTask debug print removed — it sheared concurrent serial lines and was the scenario-flake root cause; `device-list` scenario; suite 54/54) - [x] **merge** `feat/usb-xhci-bus` → main, push (merged 2026-07-13) — **plan complete** --- ## M17.1 — the kernel releases a dead process's claims The cleanup half of iron rule 1; the prerequisite for every restart story. - `system/kernel/devices-broker.zig`: `releaseAllOwnedBy(owner: u32)` — clear every `claimed[]` slot holding this task id. - `system/kernel/process.zig`: call it from the reap path, alongside the existing IRQ-binding release (the ordering comment there says why IRQs go first — claims slot in after them, before the exit notification). - MSI vectors: find where `msi_bind` records per-device vectors (interrupts module) and release those by owner in the same pass. - Docs: remove the claims bullet from process-management.md "Known gaps". **Test:** new QEMU scenario `claim-release` — a test child claims an unclaimed device, is killed, is respawned, and claims the same device again successfully; assert both claims in the serial log. Kernel-side unit coverage in `system/kernel/tests.zig` for `releaseAllOwnedBy` (claim two devices as two owners, release one owner, verify exactly its claims freed). ## M17.2 — exit reasons - `system/abi.zig`: `ExitReason` (exited, aborted, segmentation_fault, illegal_instruction, arithmetic_fault, killed). - Kernel: record the reason at every death site — clean exit path, each fault class in `onException`, the kill path. Bounded recent-exits table (ids are never reused, so a small ring keyed by id is enough). - New system call `process_exit_reason(id)` — supervisor-gated, like kill; returns the recorded reason or `-ESRCH` once evicted. - `library/runtime/process.zig`: `ExitReason` + `exitReason(id: u32)`. - Docs: remove the no-exit-status bullet from process-management.md. **Test:** extend the `supervision` scenario — three children: one exits cleanly, one faults (the fault-recovery pattern), one is killed; the supervisor asserts all three reasons. ## M17.3 — published exit events - Kernel: bounded subscriber table (endpoints); new system call `process_subscribe(endpoint)` (ungated, like `process_enumerate`); every death posts `notify_exit_bit | id` to each subscriber — the same post the supervisor path already uses. - `library/runtime/process.zig`: `subscribeExits(endpoint)`. - VFS becomes the first subscriber: on an exit event, release every handle keyed by that task id (badges already are task ids). Log the release. - Docs: note the convention in ipc.md (exit events reuse the exit-notification badge encoding). **Test:** new QEMU scenario `vfs-client-death` — a client opens a file and is killed without closing; assert the VFS logs the handle release and its open-handle count returns to baseline. ## M17.4 — signals and the service harness - Kernel: per-task pending mask + bound endpoint; system calls `signal_bind(endpoint)` and `process_signal(id, signal)` (supervisor-or-self gated); delivery posts `notify_signal_bit | pending mask`, coalescing; pending signals with no bound endpoint pend silently. - `library/runtime/process.zig`: `Signal`, `SignalSet`, `bindSignals`, `signalsFrom`, `sendSignal`, `stop(id, deadline_ms)` (terminate → wait for exit notification → kill). Implement `terminate`, `reload`, `user_1`, `user_2`; `interrupt`/`quit` are enum members with no sender yet; `alarm` stays unbuilt. - Kernel: **one-shot timer notifications** — `timer_bind(endpoint, ms)` posts a notification badge when the deadline lands (IRQ-as-IPC again, on the timer wheel `sleep` already uses). This is the missing timed-wait primitive: `replyWait` blocks forever and `sleep` blocks the whole process, but `stop()`'s escalation, the device manager's `hello` deadline (M18.1), and restart backoff all need a deadline while staying responsive. It is also the mechanism `alarm` gets for free later. - New `library/runtime/service.zig`: the harness — `run(callbacks)` owning the replyWait loop, folding protocol messages, signals, and child-exit notifications into `init` / `on_message` / `on_reload` / `on_terminate`; answers the common `ping` automatically. Define the reserved `ping` request encoding here and document it in ipc.md (one obvious encoding; smallest that cannot collide with existing protocols). - Convert one existing service (input-source or hpet) to the harness as proof it subtracts code rather than adding it. **Test:** extend `supervision` — a harness-built child: `sendSignal(reload)` observed in its log, `ping` answered, `stop()` produces a clean exit with reason `exited`; a second child that ignores signals (no bind) is killed by `stop()`'s deadline with reason `killed`. ## M18.1 — device-manager protocol: hello + restart policy - New `system/services/device-manager/device-manager-protocol.zig` module (vfs-protocol pattern): `hello { version, role, device_id }`; version constant; reserved fields. - Device manager: register the `.device_manager` endpoint; spawn drivers with its exit endpoint; enforce the hello deadline; restart policy — backoff, crash-loop cap (three fast deaths → mark failed, log, stop), reasons from M17.2 deciding restart vs not. - usb-xhci-bus: adopt the harness + send hello. hpet/ps2-bus follow only if the conversion is mechanical; otherwise they keep working unconverted (the manager only enforces hello on drivers spawned with an assignment). - build.zig: test-loop entry for the protocol module if it grows pure logic. **Test:** new QEMU scenario `driver-restart` — the xHCI driver takes a test-only argv flag to fault after hello on its first run; assert: fault, exit reason recorded, manager respawns with backoff, second run claims the controller (M17.1) and hellos clean. Assert the crash-loop cap by a driver that always faults (a tiny test driver, not xhci). ## M18.2 — bus tree reports - Protocol: `child_added { parent, identity, resources }` / `child_removed { id }`. - usb-xhci-bus: bring-up to **port scan only** — map the MMIO window (claimed in M16-era work), controller reset/start per xHCI spec, walk the port registers, report one `child_added` per connected port with speed + port number as identity. **No transfer rings, no descriptors** — reading device/interface descriptors (and therefore USB class triples for matching) is the follow-on USB track, not this plan. - Device manager: mirror reports into its tree; prune the subtree (emitting `child_removed`) when a bus driver dies; assert re-report on restart. **Test:** QEMU already attaches usb-kbd + usb-mouse on xhci.0 — assert two `child_added` events reach the manager and appear in its tree dump; kill the driver, assert two `child_removed` then two fresh `child_added` after respawn. ## M18.3 — the application surface - Protocol: `enumerate` (tree snapshot) + `subscribe` (published add/remove events, input-service pattern). - A small client (`device-list`, the `ps` analog) exercising both; the manager becomes the one answer to "what devices exist" for user space. `device_enumerate` stays for drivers/kernel seeding — its retreat is tied to the discovery migration, out of this plan. **Test:** QEMU scenario — `device-list` shows the tree including USB children; during a driver restart the subscribing client logs remove + add events. --- **Explicitly out of scope** (own tracks, after M18): discovery migration (pci-bus driver, acpi service, retiring the kernel scan), USB control transfers + descriptors + class-driver matching, the musl layer, `interrupt`/`quit` senders (needs a console), job control.