Expand description
The process-control channel — how a yah-supervised process describes itself to its supervisor and to an agent, instead of being guessed at from the outside.
§Why this is opinionated
Everything yah runs is supervised by something in the kamaji family, and until now the supervisor’s only questions were “is the pid alive?” and “is the port open?”. Both are proxies. A process that has finished booting, a process still replaying a WAL, and a process wedged on a lock all answer them identically — so the only richer signal available to an operator or an agent was grepping the log tail for a line somebody hopefully logged.
So: any process built to run under a yah camp SHOULD expose a control
channel, dev tier or cloud tier, port or no port. It is the difference
between an agent reading state = "starting", detail = "migrating 3/7"
and an agent tailing stdout hoping for a sentence.
§The contract
One required verb. A conforming process answers a status request with a
status document:
{"state":"running","ready":true,"pid":71455,"uptime_secs":41,
"detail":"3 windows open","endpoints":{"gui":"winit://main"},
"metrics":{"frames_per_sec":59.9}}state is the only required field, and its vocabulary is exactly
kamaji’s WorkloadState —
pending | starting | running | draining | exited | failed. That is the
compatibility rule that matters: the supervisor already has this enum in
its wire protocol and already answers a Probe verb with it, so a
workload that reports in the same words can be believed verbatim rather
than translated. Everything else in the document is optional.
§Two transports, one document
| Transport | Where | How |
|---|---|---|
| Unix socket | dev tier, portless or not | newline-delimited JSON: write {"cmd":"status"}\n, read one JSON line back |
| HTTP | any tier that already serves HTTP | GET <base><path> (conventionally /_yah/status) returning the same document |
The cloud tier gets this for free: a Healthcheck { probe: Http { path } }
in workload-spec pointed at the status path is the same endpoint the
dev tier reads over a socket. One document, two transports, no per-tier
fork — which is the same rule W265 applies to everything else here.
§Why newline-JSON and not the kamaji postcard wire
Because the producer side has to be implementable in twenty lines with no
dependency, in any language, by someone whose actual job that day is their
own app. kamaji’s kamaji-proto is postcard over a framed UDS: excellent
between two Rust processes that both link it, a non-starter as a thing you
ask every workload in the fleet to adopt. A strict-subset JSON document
that a Bun script or a Python daemon can emit is the version that actually
gets adopted, and it is trivially bridged into kamaji-proto’s
WorkloadState because the vocabulary was chosen to match.
§Where the socket path comes from
The supervisor picks it and hands it over in the environment as
YAH_CONTROL_SOCK. A conforming process binds $YAH_CONTROL_SOCK if
it is set and does nothing if it isn’t — so the same binary runs unchanged
outside a camp.
@arch:see(.yah/docs/working/W315-process-control-channel.md) @arch:see(.yah/docs/working/W265-service-capabilities-and-drivers.md)
@yah:ticket(R715-F3, “Process-control channel phase 2: producer helper crate, run.spawn injection, kamaji Probe bridge”)
@yah:status(review)
@yah:at(2026-08-14T22:30:16Z)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:parent(R715)
@arch:see(.yah/docs/working/W315-process-control-channel.md)
@yah:next(“Producer-side helper crate so conforming is two lines for a Rust workload (bind the socket, answer status from a Fn() -> ProcStatus). CRATE HOME IS AN OPERATOR CALL: oss/kamaji/crates/* (nearest owner, but an independent workspace and a publish surface) versus a standalone oss/procctl. It cannot live in yah-cloud where the client is, because external consumers (noisetable, in the entambi repo) need it from crates.io.”)
@yah:next(“run.spawn does not inject YAH_CONTROL_SOCK, so agent-spawned processes have no channel even if they speak it. Injecting the var is trivial; the value only appears once the camp daemon exposes a run.status RPC to read it back. Do both together.”)
@yah:next(“kamaji bridge: kamaji already answers a Probe verb with WorkloadState, and ProcState was chosen to match it word for word, but nothing wires the two. A workload that reports starting is currently believed by the reconciler and invisible to kamaji.”)
@yah:next(“Run tab polls the status once at bring-up (surfaced via RunningWorkloadSummary.notes). Live polling - a status line that moves starting -> running while you watch - needs an RPC, not new protocol.”)
@yah:next(“Nothing enforces the SHOULD. A lint over workload.toml (long-running component, no [process.control], no healthcheck) would turn W315 into a gate. One implementation was judged too little evidence to start failing builds over.”)
@yah:gotcha(“The protocol is deliberately dependency-free (one newline-delimited JSON verb), so nothing REQUIRES a crate to conform. The helper is ergonomics, not a gate - do not let its crate-home question block anyone from implementing the channel by hand.”)
@yah:gotcha(“Do not switch the wire to kamaji-proto’s postcard framing to unify them. That was considered and rejected in W315: the producer side has to be implementable in twenty lines in any language, and postcard-over-framed-UDS is a Rust-links-the-crate contract.”)
@yah:next(“DECIDED 2026-08-14 by the operator, do not re-litigate: the producer helper crate lives at oss/kamaji/crates/procctl. Kamaji already owns the WorkloadState vocabulary this protocol reuses, so helper and enum move together; accepted cost is one more publish surface in kamaji’s workspace. Rejected alternative: a standalone oss/procctl. Note kamaji is an INDEPENDENT cargo workspace with an export mirror - no workspace = true inheritance from yah’s root, and the crate ships outward via scripts/export-oss.sh.”)
@yah:handoff(“All three titled items shipped. (1) PRODUCER CRATE: oss/kamaji/crates/procctl, package kamaji-procctl, lib name procctl (dir per the operator’s call; package name follows the kamaji-* convention and is now listed in scripts/reserve-crate-names.sh + scripts/set-trusted-publishers.sh). Default build is std + serde only - a winit GUI with no runtime can adopt it, which was the motivating case. serve_env() returns Ok(None) when YAH_CONTROL_SOCK is unset; the server stamps pid and uptime when the producer omits them; a dead predecessor socket is reclaimed but a LIVE one is refused rather than stolen. Optional features: client (async consumer, tokio) and kamaji (ProcState -> WorkloadState).”)
@yah:handoff(“(2) KAMAJI BRIDGE: procctl’s kamaji feature holds the From impl as an exhaustive match, so a state added to either vocabulary stops compiling - that compile error is the entire mechanism keeping W315’s believed-verbatim claim true. The reverse direction is deliberately absent (WorkloadState is non_exhaustive, so matching it needs a wildcard, which is the silent drift the design refuses). ProbeTarget gained control: Option
Structs§
- Proc
Status - A workload’s self-description. Only
Self::stateis required.
Enums§
- Control
Endpoint - Where to ask for the status document.
- Proc
State - Lifecycle vocabulary of a supervised process.
- Ready
Outcome - Outcome of waiting for a process to report itself ready.
Constants§
- CONTROL_
SOCK_ ENV - Environment variable naming the control socket a supervised process should bind. Absent → the process is not running under a supervisor that wants a control channel, and MUST NOT fail for its absence.
- DEFAULT_
HTTP_ PATH - Conventional HTTP path for the status document on a process that already
serves HTTP. Not enforced —
[process.control] http_pathoverrides it — but a service with no reason to differ should use this one.
Functions§
- fetch_
status - Ask a process for its status document once.
- wait_
ready - Poll
endpointuntil the process reports ready, reports terminal, or the timeout expires.