Condense the documentation and thin the server's comments

The markdown had accumulated a lot that was stale rather than wrong.
PLAN.md still described pi as the llama.cpp harness, a refcounted
LlamaServerManager, and a providers-by-hosts cross-product, all of which
were superseded or never built; it also carried a second copy of the HTTP
table that routes.rs owns. EXPLORER.md and TRANSCRIPT_CACHE.md held
implementation checklists for work that has since landed. AGENTS.md
restated most of PLAN.md's design instead of being the working-notes
layer it says it is. 3225 lines of markdown to 2180, with the stale
sections gone rather than reworded.

On the server, comments explaining what the code already says are out and
the ones recording a constraint, a measurement or an incident are kept but
cut to a few lines each: 5504 comment lines to 4586.

Four doc comments in session/mod.rs, and one each in process.rs and
usage.rs, had drifted onto the item above the one they describe --
functions were reordered without them, so `stop_session`'s doc sat on
`set_session_cwd`, `stat_of`'s on `struct Stat`, and `UsageMonitor`'s on
`type Cached`. Each is back on its own item.

routes.rs's module table also claimed later phases would add `/hosts`,
which setups replaced.

cargo test (127 passed), clippy --all-targets and fmt are clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-04 15:45:43 -04:00
1 parent e3e02d55f7
commit 79682f03a7
24 files changed
+4572 -6821

No files matched your search

+113 -148
View File
@@ -2,31 +2,26 @@
//! written down so a *later* run of this server can find the same process
//! rather than start a second one.
//!
//! The server deliberately outlives its own restarts badly and its
//! children well: stopping the backend must not kill a turn that is in
//! flight, so session processes are left running and adopted again on the
//! way back up. That only works if "is this still mine?" has an answer,
//! which is what this module is.
//! Stopping the backend must not kill a turn that is in flight, so session
//! processes are left running and adopted again on the way back up. That only
//! works if "is this still mine?" has an answer, which is what this module is.
//!
//! **A pid is not an identity.** Pids are reused, so adopting one by
//! number alone eventually means treating a stranger's process as a
//! session -- never resuming the real conversation, and signalling
//! something unrelated when the session is deleted. The kernel's start
//! time for that pid is recorded beside it; the pair is unique for as long
//! as the machine has been up, which is longer than any of this lives.
//! **A pid is not an identity.** Pids are reused, so adopting one by number
//! alone eventually means treating a stranger's process as a session -- never
//! resuming the real conversation, and signalling something unrelated when the
//! session is deleted. The kernel's start time for that pid is recorded beside
//! it; the pair is unique for as long as the machine has been up.
//!
//! **How to reach it again belongs here too**, in the same record and the
//! same write, because it answers the other half of the same question: not
//! just "is my process still there" but "where do I pick it up". Splitting
//! them would be two files that can disagree about one process. What that
//! takes differs by driver -- a reading position into a log for one spoken
//! to over stdio, a port for one spoken to over HTTP -- so it is a typed
//! [`Detail`] rather than a union of every driver's fields.
//! **How to reach it again belongs here too**, in the same record and the same
//! write, because it answers the other half of the same question. Splitting
//! them would be two files that can disagree about one process. What it takes
//! differs by driver, so it is a typed [`Detail`] rather than a union of every
//! driver's fields.
//!
//! The record is rewritten in place as reading advances. A crash during
//! that write leaves a record that does not parse, which is read as "no
//! live process" -- so the failure is the old behaviour (start one with
//! `--resume`) rather than a wrong adoption.
//! The record is rewritten in place as reading advances. A crash during that
//! write leaves a record that does not parse, which is read as "no live
//! process" -- so the failure is the old behaviour rather than a wrong
//! adoption.
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
@@ -40,8 +35,8 @@ const RECORD_FILE: &str = "process.json";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Record {
pub pid: u32,
/// The kernel's start time for `pid`, in clock ticks since boot. See
/// the module comment: this is what makes the pid an identity.
/// The kernel's start time for `pid`, in clock ticks since boot. See the
/// module comment: this is what makes the pid an identity.
pub started: u64,
/// What the driver needs in order to pick this process back up.
#[serde(flatten)]
@@ -52,24 +47,21 @@ pub struct Record {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Detail {
/// Spoken to over stdio, which outlives the server as files in the
/// session directory. `stdout_read` is how many bytes of the stdout
/// log have already become events: everything before it is in the
/// transcript, everything after it is what a reattaching server owes
/// the conversation.
/// Spoken to over stdio, which outlives the server as files in the session
/// directory. `stdout_read` is how many bytes of the stdout log have
/// already become events: everything after it is what a reattaching server
/// owes the conversation.
Stdio { stdout_read: u64 },
/// Spoken to over HTTP on a loopback port, which is all it takes to
/// find it again -- there is no stream to be partway through.
/// Spoken to over HTTP on a loopback port, which is all it takes to find
/// it again -- there is no stream to be partway through.
Http { port: u16 },
}
/// Whether a recorded process is still there.
///
/// Three answers rather than a boolean, because "I could not find out" is
/// a real one and is not the same as "no". Treating it as "no" is what
/// would start a second process against a conversation that already has
/// one -- the expensive mistake this whole module exists to prevent -- so
/// it has to be sayable.
/// Three answers rather than a boolean, because "I could not find out" is a
/// real one and is not the same as "no". Treating it as "no" is what would
/// start a second process against a conversation that already has one.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Liveness {
Alive,
@@ -78,9 +70,9 @@ pub enum Liveness {
}
impl Record {
/// The record for a process this server just started, or `None` when
/// the kernel will not say when it started -- which is the same
/// answer as "do not adopt this later", and the safe one.
/// The record for a process this server just started, or `None` when the
/// kernel will not say when it started -- which is the same answer as "do
/// not adopt this later", and the safe one.
pub fn of(pid: u32, detail: Detail) -> Option<Self> {
Some(Self {
pid,
@@ -89,12 +81,9 @@ impl Record {
})
}
/// Whether the process this describes is still the one running under
/// that pid.
pub fn liveness(&self) -> Liveness {
match stat_of(self.pid) {
// A different start time is a reused pid, which is a different
// process and so definitely not ours.
// A different start time is a reused pid, so definitely not ours.
Ok(Some(stat)) if stat.started == self.started => {
if stat.exited {
Liveness::Dead
@@ -112,12 +101,10 @@ fn path(session_dir: &Path) -> PathBuf {
session_dir.join(RECORD_FILE)
}
/// The recorded process and whether it is still there, or `None` when
/// nothing usable is recorded.
///
/// A record that does not parse reads as no record: the only way to get
/// one is a crash partway through writing it, and the safe reading of that
/// is that this server has no claim on anything.
/// The recorded process and whether it is still there, or `None` when nothing
/// usable is recorded. A record that does not parse reads as no record: the
/// only way to get one is a crash partway through writing it, and the safe
/// reading is that this server has no claim on anything.
pub fn recorded(session_dir: &Path) -> Option<(Record, Liveness)> {
let text = std::fs::read_to_string(path(session_dir)).ok()?;
let record: Record = serde_json::from_str(text.trim_end()).ok()?;
@@ -125,10 +112,8 @@ pub fn recorded(session_dir: &Path) -> Option<(Record, Liveness)> {
Some((record, liveness))
}
/// The recorded process if it is definitely still running.
///
/// One function rather than a read plus a liveness check at each caller:
/// every caller wants the same question answered, and the one that forgets
/// The recorded process if it is definitely still running. One function rather
/// than a read plus a liveness check at each caller: the caller that forgets
/// the second half is the one that starts a duplicate.
pub fn live(session_dir: &Path) -> Option<Record> {
match recorded(session_dir) {
@@ -137,25 +122,20 @@ pub fn live(session_dir: &Path) -> Option<Record> {
}
}
/// Writes `record` where [`live`] will find it, atomically.
/// Writes `record` where [`live`] will find it, atomically -- to a neighbouring
/// file, renamed over the real name, so a reader sees either the whole old
/// record or the whole new one.
///
/// Written to a neighbouring file and renamed over the real name. The
/// rename is what makes this safe: a reader sees either the whole old
/// record or the whole new one, never a partial.
/// Writing in place would not be, and the consequence is severe rather than
/// untidy. `fs::write` truncates before it fills, so a crash inside that window
/// leaves no readable record -- and a missing record reads as "nothing is
/// running", which is the single answer that makes the next launch start a
/// *second* process against a conversation that already has one. The window is
/// not rare: this runs on every read that makes progress, so many times a
/// second while a turn is producing output.
///
/// Writing in place would not be, and the consequence is severe rather
/// than untidy. `fs::write` truncates before it fills, so a crash inside
/// that window leaves no readable record -- and a missing record reads as
/// "nothing is running", which is the single answer that makes the next
/// launch start a *second* process against a conversation that already has
/// one. That is the fault this whole module exists to prevent, and writing
/// the record carelessly would reintroduce it at its own save point. The
/// window is not rare either: this runs on every read that makes progress,
/// so many times a second while a turn is producing output.
///
/// Errors are logged rather than returned: this runs on the reading path,
/// and a session that cannot save its position is still worth having -- it
/// just cannot be reattached to, which is what the log says.
/// Errors are logged rather than returned: this runs on the reading path, and a
/// session that cannot save its position is still worth having.
pub fn write(session_dir: &Path, record: &Record) {
let path = path(session_dir);
let text = match serde_json::to_string(record) {
@@ -165,8 +145,8 @@ pub fn write(session_dir: &Path, record: &Record) {
return;
}
};
// Beside the real file so the rename stays within one filesystem,
// which is what makes it atomic.
// Beside the real file so the rename stays within one filesystem, which is
// what makes it atomic.
let temp = path.with_extension("json.new");
let written = std::fs::OpenOptions::new()
.create(true)
@@ -190,11 +170,8 @@ pub fn write(session_dir: &Path, record: &Record) {
}
}
/// How many bytes `path` holds, or 0 if it is not there.
///
/// Exists so a caller wanting only the length does not have to read the
/// file to find it -- [`read_from`] with a large offset answers the
/// question, but allocates the whole file on the way.
/// How many bytes `path` holds, or 0 if it is not there. Exists so a caller
/// wanting only the length does not have to read the file to find it.
pub fn size_of(path: &Path) -> u64 {
std::fs::metadata(path).map(|meta| meta.len()).unwrap_or(0)
}
@@ -211,19 +188,16 @@ pub fn clear(session_dir: &Path) {
}
/// Grace period between asking a session's process to stop and killing it.
///
/// Here rather than beside each caller: it is a property of stopping one of
/// these, and two drivers plus the manager had written the same five seconds
/// down separately, which is three places for it to drift.
/// Here rather than beside each caller: two drivers plus the manager had
/// written the same five seconds down separately.
pub const STOP_GRACE: std::time::Duration = std::time::Duration::from_secs(5);
/// Asks it to stop, then makes sure. Used where a leaked process must
/// actually end: a deleted session, or one being replaced.
/// Asks it to stop, then makes sure. Used where a leaked process must actually
/// end: a deleted session, or one being replaced.
///
/// SIGTERM first because the CLI writes its own session file on the way
/// out and a SIGKILL would cost whatever it had not flushed; SIGKILL after
/// the grace period because a session the phone has deleted must not still
/// be running when it looks again.
/// SIGTERM first because the CLI writes its own session file on the way out and
/// a SIGKILL would cost whatever it had not flushed; SIGKILL after the grace
/// period because a session the phone has deleted must not still be running.
pub fn stop(record: &Record, grace: std::time::Duration) {
if record.liveness() != Liveness::Alive {
return;
@@ -236,23 +210,20 @@ pub fn stop(record: &Record, grace: std::time::Duration) {
});
}
/// Waits for processes already asked to stop, and kills whichever have
/// not, for a caller that is about to exit.
/// Waits for processes already asked to stop, and kills whichever have not, for
/// a caller that is about to exit.
///
/// The waiting cannot be [`stop`]'s here, and that is the whole reason
/// this exists: the kill it leaves behind is a timer inside the tokio
/// runtime, and a runtime that is shutting down never runs it. That is
/// how the backend's original `shutdown_all` leaked the processes it had
/// just asked to stop -- it reported them stopped, too, which is worse
/// than not asking.
/// The waiting cannot be [`stop`]'s here, and that is the whole reason this
/// exists: the kill it leaves behind is a timer inside the tokio runtime, and a
/// runtime that is shutting down never runs it. That is how the original
/// `shutdown_all` leaked the processes it had just asked to stop -- it reported
/// them stopped, too, which is worse than not asking.
///
/// One deadline for all of them rather than one each: they were signalled
/// together, so waiting is bounded by the grace period however many there
/// are, and a server does not sit for a minute on the way out.
/// together, so waiting is bounded by the grace period however many there are.
pub fn wait_gone(records: &[Record], grace: std::time::Duration) {
/// How often to look. Short enough that the ordinary case -- a
/// process that goes at once -- costs nothing noticeable, and long
/// enough not to spin.
/// How often to look. Short enough that a process that goes at once costs
/// nothing noticeable, and long enough not to spin.
const LOOK: std::time::Duration = std::time::Duration::from_millis(20);
let deadline = std::time::Instant::now() + grace;
@@ -264,11 +235,10 @@ pub fn wait_gone(records: &[Record], grace: std::time::Duration) {
}
}
/// The end of both paths above: a process that was asked to stop and did
/// not is killed. Written once because the two callers differ only in how
/// they wait, and a grace period that means one thing in one of them and
/// something else in the other is exactly the drift `STOP_GRACE` was
/// gathered here to prevent.
/// The end of both paths above: a process that was asked to stop and did not is
/// killed. Written once because the two callers differ only in how they wait,
/// and a grace period meaning one thing in one and something else in the other
/// is exactly the drift `STOP_GRACE` was gathered here to prevent.
fn kill_if_still_there(record: &Record, grace: std::time::Duration) {
if record.liveness() == Liveness::Alive {
tracing::warn!(
@@ -281,61 +251,56 @@ fn kill_if_still_there(record: &Record, grace: std::time::Duration) {
}
fn signal(pid: u32, signal: libc::c_int) {
// SAFETY: `kill` with a positive pid touches only that process, and
// the pid came from a record whose start time was just confirmed to
// match -- so it is still the process this server started, not a
// reused number. A failure (already gone) is nothing to act on.
// SAFETY: `kill` with a positive pid touches only that process, and the pid
// came from a record whose start time was just confirmed to match -- so it
// is still the process this server started, not a reused number. A failure
// (already gone) is nothing to act on.
unsafe {
libc::kill(pid as libc::pid_t, signal);
}
}
/// The kernel's start time for `pid`, in clock ticks since boot.
///
/// Field 22 of `/proc/<pid>/stat`, counted from the closing parenthesis of
/// field 2 rather than from the start of the line: a process's name is
/// field 2, it is wrapped in parentheses, and it may itself contain spaces
/// and parentheses. Splitting the whole line on whitespace therefore reads
/// the wrong field for anything with a space in its name.
///
/// Three outcomes, and they are not the same: `Ok(None)` is "no such
/// process", `Err` is "could not find out". Collapsing the second into the
/// first is what would let a machine without a readable `/proc` look like
/// a machine with nothing running on it. Linux-specific, like `import`'s
/// use of GNU `stat`.
/// What `/proc` says about a pid.
struct Stat {
/// The kernel's start time in clock ticks since boot -- see
/// [`Record::started`].
started: u64,
/// State `Z`: the process has ended, and the kernel is keeping its
/// entry only until somebody collects the exit status.
/// State `Z`: the process has ended, and the kernel is keeping its entry
/// only until somebody collects the exit status.
///
/// Read rather than ignored, because the entry it leaves behind has
/// the same pid *and* the same start time, so a process that has
/// plainly finished goes on answering "still there" for as long as
/// nothing reaps it. None of this module's callers want that answer: a
/// session whose CLI has exited is over whether or not the status has
/// been collected, and reporting it alive makes `Exited` unsayable --
/// the session shows `unknown`, its Start button never appears, and
/// stopping it says there is nothing to stop.
/// Read rather than ignored, because that entry has the same pid *and* the
/// same start time, so a finished process goes on answering "still there"
/// for as long as nothing reaps it -- which makes `Exited` unsayable: the
/// session shows `unknown`, its Start button never appears, and stopping it
/// says there is nothing to stop.
exited: bool,
}
/// The kernel's start time for `pid`, in clock ticks since boot.
///
/// Field 22 of `/proc/<pid>/stat`, counted from the closing parenthesis of
/// field 2 rather than from the start of the line: a process's name is field 2,
/// it is wrapped in parentheses, and it may itself contain spaces and
/// parentheses. Splitting the whole line on whitespace reads the wrong field
/// for anything with a space in its name.
///
/// Three outcomes, and they are not the same: `Ok(None)` is "no such process",
/// `Err` is "could not find out". Collapsing the second into the first is what
/// would let a machine without a readable `/proc` look like a machine with
/// nothing running on it. Linux-specific, like `import`'s use of GNU `stat`.
fn stat_of(pid: u32) -> std::io::Result<Option<Stat>> {
let stat = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
Ok(stat) => stat,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err),
};
// A `/proc` entry that exists but does not have the shape this reads
// is not a process that has gone away; it is a reading this code
// cannot make, which is the other thing entirely.
// A `/proc` entry that exists but does not have the shape this reads is not
// a process that has gone away; it is a reading this code cannot make.
let unreadable =
|| std::io::Error::new(std::io::ErrorKind::InvalidData, "unreadable /proc stat");
let after_name = stat.rsplit_once(')').ok_or_else(unreadable)?.1;
// Field 3 is the first after the name, so the state is the first here
// and field 22 is the 20th.
// Field 3 is the first after the name, so the state is the first here and
// field 22 is the 20th.
let mut fields = after_name.split_whitespace();
let exited = fields.next().ok_or_else(unreadable)? == "Z";
let started = fields
@@ -346,9 +311,9 @@ fn stat_of(pid: u32) -> std::io::Result<Option<Stat>> {
Ok(Some(Stat { started, exited }))
}
/// Reads `path` from `from`, returning what is there and where reading
/// reached. A file that has been truncated or replaced under us reads from
/// the start, since the offset no longer means anything in it.
/// Reads `path` from `from`, returning what is there and where reading reached.
/// A file truncated or replaced under us reads from the start, since the offset
/// no longer means anything in it.
pub fn read_from(path: &Path, from: u64) -> Result<(Vec<u8>, u64)> {
use std::io::{Read, Seek, SeekFrom};
let mut file = match std::fs::File::open(path) {
@@ -380,8 +345,8 @@ mod tests {
.expect("this process has a start time");
assert_eq!(mine.liveness(), Liveness::Alive);
// The same pid with a different start time is a different process
// -- which is the whole reason the start time is recorded.
// The same pid with a different start time is a different process --
// which is the whole reason the start time is recorded.
let recycled = Record {
started: mine.started + 1,
..mine.clone()
@@ -416,8 +381,8 @@ mod tests {
let dir = tempfile::tempdir().expect("tempdir");
let mut record =
Record::of(std::process::id(), Detail::Stdio { stdout_read: 0 }).expect("start time");
// Rewritten the way the reader rewrites it: constantly, as the
// position advances. Each one must land whole.
// Rewritten the way the reader rewrites it: constantly, as the position
// advances. Each one must land whole.
for read in [1u64, 4096, 2, 999_999] {
record.detail = Detail::Stdio { stdout_read: read };
write(dir.path(), &record);
@@ -427,8 +392,8 @@ mod tests {
"after offset {read}"
);
}
// The rename is what makes it atomic; a leftover neighbour would
// mean it had not happened.
// The rename is what makes it atomic; a leftover neighbour would mean it
// had not happened.
let stray: Vec<_> = std::fs::read_dir(dir.path())
.expect("read dir")
.filter_map(Result::ok)
@@ -468,8 +433,8 @@ mod tests {
assert_eq!(bytes, b"world");
assert_eq!(read, 11);
// An offset past the end means the file was replaced, so the
// offset describes a file that no longer exists.
// An offset past the end means the file was replaced, so the offset
// describes a file that no longer exists.
std::fs::write(&path, b"new").expect("truncate");
let (bytes, read) = read_from(&path, 11).expect("read");
assert_eq!(bytes, b"new");