Keep a typed path as typed, and expand ~ where it is used

A llama session's tools all answered "failed to spawn process
[exit code: -1]": `llama-server` takes the working directory as an
`x-tool-cwd` header and `chdir`s to it with no shell in the way, so a
`~/…` cwd named a directory of that name. `files::resolve_blocking`
asks the machine that will serve the session what the path is, and the
driver does that once at launch.

The other half is the storing. `~` and `/home/someone` are a path and a
snapshot of where it pointed, and it is the snapshot that breaks when an
account is renamed -- so `machines::tidy` no longer expands one and
`set_cwd` no longer contracts one (`shorten_home` is gone with it). The
identity file is expanded at the point `ssh` is invoked instead.

Spawn now asks the same question of a typed working directory that
`set_cwd` already did: absolute or home-relative, and actually there on
the machine that will run it. It accepted anything, so a typo became a
session whose process could not start, reported later and pointing at
nothing.

Also: a provider written before `mcp_servers` existed adopts the
defaults a probe would give it now, so a machine discovered before
2026-09-19 stops silently having no web search.

Exercised end to end against a real llama session spawned with
`cwd: "~/repos/ai-app/server"`: `exec_shell_command` with `pwd` answered
`/home/bob/repos/ai-app/server`, exit 0.
This commit is contained in:
iris-ai committed 2026-09-20 17:06:13 -04:00
1 parent bd9596d782
commit 3b309766d7
8 files changed
+201 -101

No files matched your search

+23 -7
View File
@@ -378,9 +378,11 @@ struct Shared {
/// are changeable while the session runs: they ride on the next request,
/// so unlike the server's own flags there is nothing to reload.
sampling: Mutex<serde_json::Map<String, Value>>,
/// The session's working directory, which is where its tools act. `None`
/// leaves that to `llama-server`, which is the honest answer rather than a
/// guess at one.
/// The session's working directory, which is where its tools act, resolved
/// on the machine serving this session. Absolute, because it is handed to
/// `llama-server` as a header it `chdir`s to with no shell to expand a
/// `~`. `None` leaves the directory to `llama-server`, which is the honest
/// answer rather than a guess at one.
cwd: Option<String>,
/// Set by [`Driver::interrupt`]; the streaming loop and the tool loop both
/// check it, leaving what was produced in the transcript.
@@ -474,16 +476,30 @@ impl LlamaDriver {
let sampling = sampling_from(&meta.params);
// Resolved on the machine that will serve this session, because it is
// the one path here that reaches something with no shell in front of
// it: `llama-server` `chdir`s to the header it is given. See
// `files::resolve_blocking`. A failure is the launch's, for the same
// reason a CLI session whose directory has gone fails at its own `cd`
// -- tools running somewhere other than where they were told to is
// worse than not starting.
let cwd = meta
.cwd
.as_ref()
.map(|path| {
crate::files::resolve_blocking(transport, &path.to_string_lossy()).with_context(
|| format!("this session's working directory, {}", path.display()),
)
})
.transpose()?;
let driver = Self {
shared: Arc::new(Shared {
sink,
transcript: transcript.to_path_buf(),
session_dir: session_dir.to_path_buf(),
sampling: Mutex::new(sampling),
cwd: meta
.cwd
.as_ref()
.map(|path| path.to_string_lossy().into_owned()),
cwd,
cancel: AtomicBool::new(false),
turns: Mutex::new(Turns::default()),
serving: Mutex::new(Serving::Loading),
+6
View File
@@ -172,6 +172,12 @@ impl Tools {
/// say they use one. A session with no working directory sends none, and
/// `llama-server` falls back to its own -- which is the honest outcome:
/// this server has no better answer for where "here" is.
///
/// It must already be **absolute**: the header is passed to `chdir` with
/// no shell in the way, so a leading `~` is a directory of that name and
/// every tool using one fails with "failed to spawn process". The driver
/// resolves it on the serving machine at launch; see
/// `files::resolve_blocking`.
pub fn execute(
&self,
name: &str,