diff --git a/CHANGELOG.md b/CHANGELOG.md index 38cffa5..be1ad21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.8.1] — 2026-10-09 +### Fixed +- The usage report no longer renders an absent per-step `messages` count as a measured `0`; it is `null` in the status JSON when unknown (found by the pre-tag scan). +- `read_file` rejects an empty `path` with a clear message instead of a confusing "is a directory" error. + ### Added - `tole status` and the serve status JSON now report token usage, cache-hit rate and a per-step request-size split (system+tools vs history, stored per step under `tole_wire` in the usage record); `fact/mission` gains `cached_tokens`. Instrumentation only: the wire, budgets and durable entry shapes are unchanged (#211, part 1 of 3). diff --git a/crates/tole-cli/src/usage_report.rs b/crates/tole-cli/src/usage_report.rs index cbdec1f..509c25a 100644 --- a/crates/tole-cli/src/usage_report.rs +++ b/crates/tole-cli/src/usage_report.rs @@ -22,7 +22,9 @@ pub struct WirePoint { pub system_chars: u64, pub tools_chars: u64, pub history_chars: u64, - pub messages: u64, + /// Message count of the request; `None` when the record does not carry it + /// (unknown is never rendered as a measured zero). + pub messages: Option, } #[derive(Debug, Clone, PartialEq)] @@ -101,7 +103,7 @@ fn wire_of(u: &Value) -> Option { system_chars, tools_chars, history_chars, - messages: w.get("messages").and_then(Value::as_u64).unwrap_or(0), + messages: w.get("messages").and_then(Value::as_u64), }) } @@ -417,4 +419,14 @@ mod tests { assert_eq!(r.prompt_tokens, 0); assert!(r.wire_first.is_none()); } + + #[test] + fn missing_messages_is_unknown_not_zero() { + let r = usage_report(&[rec( + json!({"tole_wire": {"system_chars": 1, "tools_chars": 2, "history_chars": 3}}), + )]); + let w = r.wire_first.expect("wire point without messages is kept"); + assert_eq!(w.messages, None); + assert_eq!(wire_json(&w)["messages"], Value::Null); + } } diff --git a/crates/tole-core/src/read_file.rs b/crates/tole-core/src/read_file.rs index e4016bf..ba410a0 100644 --- a/crates/tole-core/src/read_file.rs +++ b/crates/tole-core/src/read_file.rs @@ -44,6 +44,9 @@ impl ReadFileTool { /// also an escape. Errors distinguish escape from plain not-found so /// the model gets actionable feedback. fn jailed(&self, rel: &str) -> Result { + if rel.is_empty() { + return Err("read_file: path is empty".to_string()); + } let rel_path = Path::new(rel); // Component-based validation (uniform with the other file tools; // CodeCora scan #6/#12): drive-prefixed/rooted Windows relatives @@ -207,6 +210,13 @@ mod tests { assert_eq!(out["bytes"], json!(5)); } + #[test] + fn rejects_empty_path() { + let t = ReadFileTool::new(tmpdir("empty")); + let err = t.execute(json!({ "path": "" })).unwrap_err(); + assert!(err.to_string().contains("path is empty"), "{err}"); + } + #[test] fn rejects_absolute_and_traversal() { let dir = tmpdir("jail");