refactor: clear pi-era residue + make CI gate green (harness-neutral cleanup) #158

Merged
clawdie merged 1 commit from cleanup-pi-zot-residue into main 2026-06-24 00:36:29 +02:00
12 changed files with 121 additions and 76 deletions

View file

@ -102,6 +102,18 @@ cargo test --workspace
cargo build --workspace --release cargo build --workspace --release
``` ```
**Mandatory before every merge to `main`:** run the full gate via
```sh
./scripts/ci-checks.sh # fmt --check, clippy -D warnings, test, markdown gate
```
`.forgejo/workflows/ci.yml` encodes the same checks, but **no Actions runner is
currently registered**, so nothing enforces them server-side. Until a runner is
active, `ci-checks.sh` passing locally is the only gate — a green run is a
prerequisite for merging. (A compile break reached `main` once because this step
was skipped; do not skip it.)
## ISO Takeover Gates ## ISO Takeover Gates
| Gate | Target | Status | | Gate | Target | Status |

View file

@ -327,7 +327,7 @@ mod tests {
"id": "pane-1", "id": "pane-1",
"agent": "pi", "agent": "pi",
"state": "working", "state": "working",
"pi_session_id": "pi-session-1", "session_id": "pi-session-1",
"last_event_at": "2026-05-27T11:59:59.000Z", "last_event_at": "2026-05-27T11:59:59.000Z",
"cwd": "/repo" "cwd": "/repo"
}] }]

View file

@ -130,7 +130,7 @@ async fn daemon_client_live_socket_check_with_local_fake_agent() {
.iter() .iter()
.find(|pane| pane.id == agent_id) .find(|pane| pane.id == agent_id)
.unwrap(); .unwrap();
assert_eq!(pane.pi_session_id.as_deref(), Some("manual-test")); assert_eq!(pane.session_id.as_deref(), Some("manual-test"));
assert!(pane.cwd.is_some()); assert!(pane.cwd.is_some());
let kill = client.kill_agent(agent_id).await.unwrap(); let kill = client.kill_agent(agent_id).await.unwrap();
@ -370,7 +370,7 @@ async fn harness_double_spawn_session_isolation() {
assert_ne!(pane_a.id, pane_b.id); assert_ne!(pane_a.id, pane_b.id);
// Verify session isolation: both share the same session_id (test agent default) // Verify session isolation: both share the same session_id (test agent default)
assert_eq!(pane_a.pi_session_id, pane_b.pi_session_id); assert_eq!(pane_a.session_id, pane_b.session_id);
// Kill one agent — snapshot may still include stopped panes briefly // Kill one agent — snapshot may still include stopped panes briefly
let kill = client.kill_agent(&pane_a.id).await.unwrap(); let kill = client.kill_agent(&pane_a.id).await.unwrap();

View file

@ -49,6 +49,8 @@ pub struct RuntimeInventory {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub pi: Option<String>, pub pi: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub zot: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub npm_prefix: Option<String>, pub npm_prefix: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub package_manager: Option<String>, pub package_manager: Option<String>,

View file

@ -74,7 +74,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let socket_handle = let socket_handle =
tokio::spawn(async move { socket::serve(socket_state, socket_shutdown).await }); tokio::spawn(async move { socket::serve(socket_state, socket_shutdown).await });
// Auto-spawn one Pi agent if configured (live "Operator Image" OOTB flow). // Auto-spawn one agent if configured (live "Operator Image" OOTB flow).
// Runs after the socket server is up so the spawn registers on the board; // Runs after the socket server is up so the spawn registers on the board;
// no-op unless COLIBRI_AUTOSPAWN is set and a DeepSeek key is present. // no-op unless COLIBRI_AUTOSPAWN is set and a DeepSeek key is present.
socket::autospawn_agent_if_configured(&state).await; socket::autospawn_agent_if_configured(&state).await;

View file

@ -416,9 +416,7 @@ pub async fn autospawn_agent_if_configured(state: &SharedState) {
return; return;
} }
if state.config.deepseek_api_key.is_none() { if state.config.deepseek_api_key.is_none() {
info!( info!("autospawn: DEEPSEEK_API_KEY not set; skipping (operator can add it via Join Hive)");
"autospawn: DEEPSEEK_API_KEY not set; skipping (operator can add it via Join Hive)"
);
return; return;
} }
@ -438,22 +436,13 @@ pub async fn autospawn_agent_if_configured(state: &SharedState) {
return; return;
} }
// Default argv depends on the harness: zot is a request/response peer // Default argv depends on the harness (see default_agent_args). Override
// driven over stdin (`zot rpc`), while pi self-drives (`pi --mode json`). // wholesale with COLIBRI_AUTOSPAWN_ARGS.
// `--mode json` is NOT a valid zot flag, so a single default would break one
// of them — pick per binary. Override with COLIBRI_AUTOSPAWN_ARGS.
let default_args = if agent_name == "zot" {
"rpc"
} else {
"--mode json"
};
let args: Vec<String> = std::env::var("COLIBRI_AUTOSPAWN_ARGS") let args: Vec<String> = std::env::var("COLIBRI_AUTOSPAWN_ARGS")
.ok() .ok()
.filter(|s| !s.trim().is_empty()) .filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| default_args.to_string()) .map(|s| s.split_whitespace().map(str::to_string).collect())
.split_whitespace() .unwrap_or_else(|| default_agent_args(&agent_binary));
.map(str::to_string)
.collect();
info!(binary = %agent_binary, ?args, "autospawn: spawning agent on host (DeepSeek-backed)"); info!(binary = %agent_binary, ?args, "autospawn: spawning agent on host (DeepSeek-backed)");
@ -554,6 +543,20 @@ fn basename(path: &str) -> String {
.to_string() .to_string()
} }
/// Default argv for an agent harness, by binary basename. zot is an RPC peer
/// (`zot rpc`, driven over stdin); every other harness is assumed to be a
/// self-driving JSON emitter (`pi --mode json`). `--mode json` is not a valid
/// zot flag, so the default must vary by harness — a single shared default
/// would break one of them. Single source of truth for both autospawn and the
/// non-local spawn path.
fn default_agent_args(binary: &str) -> Vec<String> {
if basename(binary) == "zot" {
vec!["rpc".to_string()]
} else {
vec!["--mode".to_string(), "json".to_string()]
}
}
fn env_truthy(name: &str) -> bool { fn env_truthy(name: &str) -> bool {
std::env::var(name) std::env::var(name)
.map(|v| { .map(|v| {
@ -565,6 +568,10 @@ fn env_truthy(name: &str) -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
// Spawn parameters are passed positionally to mirror the socket command shape;
// grouping them into a struct would not improve clarity here. Matches the same
// allow on prepare_spawn_command.
#[allow(clippy::too_many_arguments)]
async fn cmd_spawn_agent( async fn cmd_spawn_agent(
state: &SharedState, state: &SharedState,
provider_str: String, provider_str: String,
@ -590,10 +597,12 @@ async fn cmd_spawn_agent(
let args = local_args.unwrap_or_default(); let args = local_args.unwrap_or_default();
(model.clone(), args) (model.clone(), args)
} else { } else {
( // Non-local provider: a managed agent harness. Default to zot (the
std::env::var("COLIBRI_AGENT_BINARY").unwrap_or_else(|_| "hermes-agent".to_string()), // current default harness); argv follows the harness via
vec!["--mode".to_string(), "json".to_string()], // default_agent_args so we never spawn `zot --mode json`.
) let binary = std::env::var("COLIBRI_AGENT_BINARY").unwrap_or_else(|_| "zot".to_string());
let args = default_agent_args(&binary);
(binary, args)
}; };
// An agent invoked in RPC mode (`zot rpc` / `--rpc`) blocks on stdin until // An agent invoked in RPC mode (`zot rpc` / `--rpc`) blocks on stdin until
@ -1093,7 +1102,7 @@ mod tests {
let data = response.data.unwrap(); let data = response.data.unwrap();
assert_eq!(data["schema"], "clawdie.glasspane.snapshot.v1"); assert_eq!(data["schema"], "clawdie.glasspane.snapshot.v1");
assert_eq!(data["panes"][0]["id"], "pane-a"); assert_eq!(data["panes"][0]["id"], "pane-a");
assert_eq!(data["panes"][0]["pi_session_id"], "pi-s"); assert_eq!(data["panes"][0]["session_id"], "pi-s");
} }
#[tokio::test] #[tokio::test]
@ -1130,7 +1139,7 @@ mod tests {
.find(|pane| pane.id == pane_id) .find(|pane| pane.id == pane_id)
.unwrap(); .unwrap();
assert_eq!(pane.state, colibri_glasspane::AgentState::Done); assert_eq!(pane.state, colibri_glasspane::AgentState::Done);
assert_eq!(pane.pi_session_id.as_deref(), Some("pi-fake")); assert_eq!(pane.session_id.as_deref(), Some("pi-fake"));
} }
#[tokio::test] #[tokio::test]

View file

@ -317,11 +317,7 @@ impl RpcSender {
/// Send one prompt as a newline-delimited request on the agent's stdin. /// Send one prompt as a newline-delimited request on the agent's stdin.
/// Returns the request id used. Errors if the stdin pipe has closed. /// Returns the request id used. Errors if the stdin pipe has closed.
pub async fn send_prompt(&self, message: &str) -> std::io::Result<String> { pub async fn send_prompt(&self, message: &str) -> std::io::Result<String> {
let id = (self let id = (self.seq.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1).to_string();
.seq
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
+ 1)
.to_string();
let line = build_rpc_prompt(&id, message); let line = build_rpc_prompt(&id, message);
let mut guard = self.stdin.lock().await; let mut guard = self.stdin.lock().await;
let stdin = guard.as_mut().ok_or_else(|| { let stdin = guard.as_mut().ok_or_else(|| {
@ -552,7 +548,7 @@ pub fn jail_wrap(
/// Configuration for spawning an agent subprocess. /// Configuration for spawning an agent subprocess.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSpawnConfig { pub struct AgentSpawnConfig {
/// Agent binary or command to run (e.g. "pi", "hermes-agent", etc.). /// Agent binary or command to run (e.g. "zot", "pi", etc.).
pub binary: String, pub binary: String,
/// Command-line arguments. /// Command-line arguments.
#[serde(default)] #[serde(default)]

View file

@ -29,7 +29,7 @@ mod integration {
ingestor.ingest_line_at(line, t + Duration::from_secs(i as u64)); ingestor.ingest_line_at(line, t + Duration::from_secs(i as u64));
} }
assert_eq!(ingestor.state(), AgentState::Done); assert_eq!(ingestor.state(), AgentState::Done);
assert_eq!(ingestor.pi_session_id(), Some("s1")); assert_eq!(ingestor.session_id(), Some("s1"));
} }
/// Agent stays idle through unknown events (no crash, forward-compatible) /// Agent stays idle through unknown events (no crash, forward-compatible)
@ -81,7 +81,7 @@ mod integration {
id: "a".into(), id: "a".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Working, state: AgentState::Working,
pi_session_id: Some("s1".into()), session_id: Some("s1".into()),
last_event_at: Some("2026-05-27T10:00:00Z".into()), last_event_at: Some("2026-05-27T10:00:00Z".into()),
cwd: None, cwd: None,
stalled: false, stalled: false,
@ -90,7 +90,7 @@ mod integration {
id: "b".into(), id: "b".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Blocked, state: AgentState::Blocked,
pi_session_id: Some("s2".into()), session_id: Some("s2".into()),
last_event_at: Some("2026-05-27T10:00:01Z".into()), last_event_at: Some("2026-05-27T10:00:01Z".into()),
cwd: None, cwd: None,
stalled: false, stalled: false,
@ -99,7 +99,7 @@ mod integration {
id: "c".into(), id: "c".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Idle, state: AgentState::Idle,
pi_session_id: None, session_id: None,
last_event_at: None, last_event_at: None,
cwd: None, cwd: None,
stalled: false, stalled: false,
@ -124,7 +124,7 @@ mod integration {
id: "p1".into(), id: "p1".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Working, state: AgentState::Working,
pi_session_id: Some("sess-1".into()), session_id: Some("sess-1".into()),
last_event_at: Some("2026-05-27T10:00:00Z".into()), last_event_at: Some("2026-05-27T10:00:00Z".into()),
cwd: None, cwd: None,
stalled: false, stalled: false,
@ -141,7 +141,7 @@ mod integration {
fn default_ingestor_is_idle() { fn default_ingestor_is_idle() {
let ingestor = PiJsonlIngestor::default(); let ingestor = PiJsonlIngestor::default();
assert_eq!(ingestor.state(), AgentState::Idle); assert_eq!(ingestor.state(), AgentState::Idle);
assert!(ingestor.pi_session_id().is_none()); assert!(ingestor.session_id().is_none());
} }
/// Queue update blocks agent /// Queue update blocks agent

View file

@ -4,7 +4,7 @@
//! Uses scripts/fake-pi-agent.py which emits the colibri-pi-events JSONL taxonomy. //! Uses scripts/fake-pi-agent.py which emits the colibri-pi-events JSONL taxonomy.
//! //!
//! With real Pi binary (when installed): //! With real Pi binary (when installed):
//! COLIBRI_PI_BINARY=pi cargo test -p colibri-daemon --test pi_spawn_live -- --nocapture //! COLIBRI_AGENT_BINARY=pi cargo test -p colibri-daemon --test pi_spawn_live -- --nocapture
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Stdio; use std::process::Stdio;
@ -79,13 +79,13 @@ async fn pi_spawn_path_produces_correct_glasspane_state() {
"expected Done after full agent run" "expected Done after full agent run"
); );
assert!( assert!(
pane.pi_session_id.is_some(), pane.session_id.is_some(),
"expected pi_session_id from session event, got {:?}", "expected session_id from session event, got {:?}",
pane.pi_session_id pane.session_id
); );
eprintln!( eprintln!(
"✓ Pi spawn proof: {} accepted, state={:?}, session={:?}", "✓ Pi spawn proof: {} accepted, state={:?}, session={:?}",
accepted, pane.state, pane.pi_session_id accepted, pane.state, pane.session_id
); );
} }

View file

@ -74,7 +74,7 @@ struct App {
// harness additions // harness additions
status_msg: Option<(String, u8)>, // (message, ttl ticks) status_msg: Option<(String, u8)>, // (message, ttl ticks)
detail_pane: Option<usize>, // index into snapshot.panes, or None detail_pane: Option<usize>, // index into snapshot.panes, or None
session_filter: Option<String>, // filter by pi_session_id, or None session_filter: Option<String>, // filter by session_id, or None
sessions: Vec<String>, // all known session IDs sessions: Vec<String>, // all known session IDs
session_idx: usize, // which session is selected session_idx: usize, // which session is selected
} }
@ -113,7 +113,7 @@ impl App {
Some(sid) => snap Some(sid) => snap
.panes .panes
.iter() .iter()
.filter(|p| p.pi_session_id.as_deref() == Some(sid.as_str())) .filter(|p| p.session_id.as_deref() == Some(sid.as_str()))
.collect(), .collect(),
None => snap.panes.iter().collect(), None => snap.panes.iter().collect(),
} }
@ -132,7 +132,7 @@ impl App {
let mut ids: Vec<String> = snap let mut ids: Vec<String> = snap
.panes .panes
.iter() .iter()
.filter_map(|p| p.pi_session_id.clone()) .filter_map(|p| p.session_id.clone())
.collect(); .collect();
ids.sort(); ids.sort();
ids.dedup(); ids.dedup();
@ -377,7 +377,7 @@ impl App {
format!("{:?}", p.state), format!("{:?}", p.state),
Style::default().fg(color), Style::default().fg(color),
)), )),
Cell::from(p.pi_session_id.as_deref().unwrap_or("")), Cell::from(p.session_id.as_deref().unwrap_or("")),
Cell::from(p.cwd.as_deref().unwrap_or("")), Cell::from(p.cwd.as_deref().unwrap_or("")),
Cell::from(if p.stalled { "" } else { "" }), Cell::from(if p.stalled { "" } else { "" }),
]) ])
@ -429,7 +429,7 @@ impl App {
), ),
Span::raw(" "), Span::raw(" "),
Span::styled("Session: ", Style::default().add_modifier(Modifier::BOLD)), Span::styled("Session: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(pane.pi_session_id.as_deref().unwrap_or("")), Span::raw(pane.session_id.as_deref().unwrap_or("")),
]), ]),
Line::from(vec![ Line::from(vec![
Span::styled("CWD: ", Style::default().add_modifier(Modifier::BOLD)), Span::styled("CWD: ", Style::default().add_modifier(Modifier::BOLD)),
@ -649,7 +649,7 @@ mod tests {
id: "a".into(), id: "a".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Working, state: AgentState::Working,
pi_session_id: Some("s1".into()), session_id: Some("s1".into()),
last_event_at: None, last_event_at: None,
cwd: None, cwd: None,
stalled: false, stalled: false,
@ -658,7 +658,7 @@ mod tests {
id: "b".into(), id: "b".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Idle, state: AgentState::Idle,
pi_session_id: Some("s2".into()), session_id: Some("s2".into()),
last_event_at: None, last_event_at: None,
cwd: None, cwd: None,
stalled: false, stalled: false,
@ -693,7 +693,7 @@ mod tests {
id: "a".into(), id: "a".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Working, state: AgentState::Working,
pi_session_id: Some("s2".into()), session_id: Some("s2".into()),
last_event_at: None, last_event_at: None,
cwd: None, cwd: None,
stalled: false, stalled: false,
@ -702,7 +702,7 @@ mod tests {
id: "b".into(), id: "b".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Working, state: AgentState::Working,
pi_session_id: Some("s1".into()), session_id: Some("s1".into()),
last_event_at: None, last_event_at: None,
cwd: None, cwd: None,
stalled: false, stalled: false,
@ -711,7 +711,7 @@ mod tests {
id: "c".into(), id: "c".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Working, state: AgentState::Working,
pi_session_id: Some("s1".into()), session_id: Some("s1".into()),
last_event_at: None, last_event_at: None,
cwd: None, cwd: None,
stalled: false, stalled: false,

View file

@ -201,7 +201,7 @@ pub fn pane_from_jsonl_with_id(
id: pane_id.into(), id: pane_id.into(),
agent: agent.into(), agent: agent.into(),
state: ingestor.state, state: ingestor.state,
pi_session_id: ingestor.pi_session_id, session_id: ingestor.session_id,
last_event_at: None, last_event_at: None,
cwd: ingestor.cwd, cwd: ingestor.cwd,
stalled: false, stalled: false,
@ -223,7 +223,7 @@ pub fn pane_from_jsonl(agent: impl Into<String>, jsonl: &str) -> Pane {
pub struct PiStreamUpdate { pub struct PiStreamUpdate {
pub pi_type: String, pub pi_type: String,
pub state: AgentState, pub state: AgentState,
pub pi_session_id: Option<String>, pub session_id: Option<String>,
pub cwd: Option<String>, pub cwd: Option<String>,
pub observed_at: SystemTime, pub observed_at: SystemTime,
} }
@ -244,7 +244,7 @@ pub struct PaneReaderStats {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct PiJsonlIngestor { pub struct PiJsonlIngestor {
state: AgentState, state: AgentState,
pi_session_id: Option<String>, session_id: Option<String>,
cwd: Option<String>, cwd: Option<String>,
last_event_at: Option<SystemTime>, last_event_at: Option<SystemTime>,
/// Which harness produced the stream. Pi events are read directly; zot /// Which harness produced the stream. Pi events are read directly; zot
@ -256,7 +256,7 @@ impl Default for PiJsonlIngestor {
fn default() -> Self { fn default() -> Self {
Self { Self {
state: AgentState::Idle, state: AgentState::Idle,
pi_session_id: None, session_id: None,
cwd: None, cwd: None,
last_event_at: None, last_event_at: None,
runtime: AgentRuntime::Pi, runtime: AgentRuntime::Pi,
@ -277,8 +277,8 @@ impl PiJsonlIngestor {
self.state self.state
} }
pub fn pi_session_id(&self) -> Option<&str> { pub fn session_id(&self) -> Option<&str> {
self.pi_session_id.as_deref() self.session_id.as_deref()
} }
pub fn cwd(&self) -> Option<&str> { pub fn cwd(&self) -> Option<&str> {
@ -312,7 +312,7 @@ impl PiJsonlIngestor {
if ty == "session" || ty == "session_started" { if ty == "session" || ty == "session_started" {
if let Some(sid) = value.get("id").and_then(Value::as_str) { if let Some(sid) = value.get("id").and_then(Value::as_str) {
self.pi_session_id = Some(sid.to_string()); self.session_id = Some(sid.to_string());
} }
if let Some(cwd) = value.get("cwd").and_then(Value::as_str) { if let Some(cwd) = value.get("cwd").and_then(Value::as_str) {
self.cwd = Some(cwd.to_string()); self.cwd = Some(cwd.to_string());
@ -322,7 +322,7 @@ impl PiJsonlIngestor {
Some(PiStreamUpdate { Some(PiStreamUpdate {
pi_type: ty.to_string(), pi_type: ty.to_string(),
state: self.state, state: self.state,
pi_session_id: self.pi_session_id.clone(), session_id: self.session_id.clone(),
cwd: self.cwd.clone(), cwd: self.cwd.clone(),
observed_at, observed_at,
}) })
@ -373,8 +373,8 @@ impl SupervisedPane {
self.ingestor.state() self.ingestor.state()
} }
pub fn pi_session_id(&self) -> Option<&str> { pub fn session_id(&self) -> Option<&str> {
self.ingestor.pi_session_id() self.ingestor.session_id()
} }
pub fn cwd(&self) -> Option<&str> { pub fn cwd(&self) -> Option<&str> {
@ -407,7 +407,7 @@ impl SupervisedPane {
id: self.id.clone(), id: self.id.clone(),
agent: self.agent.clone(), agent: self.agent.clone(),
state: self.state(), state: self.state(),
pi_session_id: self.pi_session_id().map(str::to_string), session_id: self.session_id().map(str::to_string),
last_event_at: self.last_event_at().map(system_time_to_rfc3339), last_event_at: self.last_event_at().map(system_time_to_rfc3339),
cwd: self.cwd().map(str::to_string), cwd: self.cwd().map(str::to_string),
stalled: self.is_stalled_at(now, stall_after), stalled: self.is_stalled_at(now, stall_after),
@ -627,12 +627,18 @@ fn system_time_to_rfc3339(time: SystemTime) -> String {
/// A supervised pane — one agent occupying one PTY/session slot. /// A supervised pane — one agent occupying one PTY/session slot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pane { pub struct Pane {
/// Colibri-owned pane id. Distinct from the Pi session id. /// Colibri-owned pane id. Distinct from the agent's own session id.
pub id: String, pub id: String,
pub agent: String, pub agent: String,
pub state: AgentState, pub state: AgentState,
#[serde(default, skip_serializing_if = "Option::is_none")] // `alias` keeps deserializing the legacy `pi_session_id` key (pre-zot wire
pub pi_session_id: Option<String>, // format / persisted snapshots) onto the harness-neutral field.
#[serde(
default,
alias = "pi_session_id",
skip_serializing_if = "Option::is_none"
)]
pub session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub last_event_at: Option<String>, pub last_event_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@ -737,7 +743,7 @@ mod tests {
id: "p1".into(), id: "p1".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Working, state: AgentState::Working,
pi_session_id: Some("pi-session-1".into()), session_id: Some("pi-session-1".into()),
last_event_at: None, last_event_at: None,
cwd: Some("/repo".into()), cwd: Some("/repo".into()),
stalled: false, stalled: false,
@ -746,7 +752,7 @@ mod tests {
id: "p2".into(), id: "p2".into(),
agent: "pi".into(), agent: "pi".into(),
state: AgentState::Blocked, state: AgentState::Blocked,
pi_session_id: None, session_id: None,
last_event_at: None, last_event_at: None,
cwd: None, cwd: None,
stalled: true, stalled: true,
@ -779,7 +785,7 @@ mod tests {
assert_eq!(pane.state, AgentState::Done); assert_eq!(pane.state, AgentState::Done);
assert_eq!(pane.id, "pane-a"); assert_eq!(pane.id, "pane-a");
assert_eq!( assert_eq!(
pane.pi_session_id.as_deref(), pane.session_id.as_deref(),
Some("019e5e59-6645-7e21-aca2-b57ccf0f8578") Some("019e5e59-6645-7e21-aca2-b57ccf0f8578")
); );
assert_eq!(pane.cwd.as_deref(), Some("/home/clawdija/clawdie-ai")); assert_eq!(pane.cwd.as_deref(), Some("/home/clawdija/clawdie-ai"));
@ -815,7 +821,7 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(update.pi_type, "session"); assert_eq!(update.pi_type, "session");
assert_eq!(update.observed_at, t(10)); assert_eq!(update.observed_at, t(10));
assert_eq!(ingestor.pi_session_id(), Some("pi-s")); assert_eq!(ingestor.session_id(), Some("pi-s"));
assert_eq!(ingestor.cwd(), Some("/repo")); assert_eq!(ingestor.cwd(), Some("/repo"));
assert_eq!(ingestor.last_event_at(), Some(t(10))); assert_eq!(ingestor.last_event_at(), Some(t(10)));
@ -825,7 +831,7 @@ mod tests {
} }
#[test] #[test]
fn supervisor_keeps_colibri_pane_id_separate_from_pi_session_id() { fn supervisor_keeps_colibri_pane_id_separate_from_session_id() {
let mut supervisor = PaneSupervisor::new(); let mut supervisor = PaneSupervisor::new();
supervisor.attach_pane_at("pane-1", "pi", t(0)); supervisor.attach_pane_at("pane-1", "pi", t(0));
supervisor.ingest_line_at( supervisor.ingest_line_at(
@ -840,8 +846,8 @@ mod tests {
.pop() .pop()
.unwrap(); .unwrap();
assert_eq!(pane.id, "pane-1"); assert_eq!(pane.id, "pane-1");
assert_eq!(pane.pi_session_id.as_deref(), Some("pi-session-1")); assert_eq!(pane.session_id.as_deref(), Some("pi-session-1"));
assert_ne!(pane.id, pane.pi_session_id.unwrap()); assert_ne!(pane.id, pane.session_id.unwrap());
} }
#[test] #[test]
@ -861,7 +867,7 @@ mod tests {
let pane = supervisor.get("pane-a").unwrap(); let pane = supervisor.get("pane-a").unwrap();
assert_eq!(pane.state(), AgentState::Working); assert_eq!(pane.state(), AgentState::Working);
assert_eq!(pane.pi_session_id(), Some("pi-s")); assert_eq!(pane.session_id(), Some("pi-s"));
assert_eq!( assert_eq!(
pane.last_event_at(), pane.last_event_at(),
Some(t(100) + Duration::from_millis(3)) Some(t(100) + Duration::from_millis(3))
@ -896,7 +902,7 @@ mod tests {
let pane = supervisor.get("pane-reader").unwrap(); let pane = supervisor.get("pane-reader").unwrap();
assert_eq!(pane.state(), AgentState::Done); assert_eq!(pane.state(), AgentState::Done);
assert_eq!(pane.pi_session_id(), Some("pi-s")); assert_eq!(pane.session_id(), Some("pi-s"));
assert_eq!(pane.last_event_at(), Some(t(13))); assert_eq!(pane.last_event_at(), Some(t(13)));
} }

View file

@ -21,6 +21,8 @@ struct RuntimeInventory {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pi: Option<String>, pi: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
zot: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
npm_prefix: Option<String>, npm_prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
package_manager: Option<String>, package_manager: Option<String>,
@ -156,6 +158,23 @@ fn detect_pi_version() -> Option<String> {
.find_map(|candidate| pi_package_version_from_bin(&candidate)) .find_map(|candidate| pi_package_version_from_bin(&candidate))
} }
/// Detect the installed zot agent version. zot is a single Go binary (not an
/// npm package), so this is a plain `--version` probe across `$ZOT_BIN`, PATH,
/// and the usual candidate locations.
fn detect_zot_version() -> Option<String> {
if let Ok(zot_bin) = env::var("ZOT_BIN") {
if let Some(version) = command_output(&zot_bin, &["--version"]) {
return Some(version);
}
}
if let Some(version) = command_output("zot", &["--version"]) {
return Some(version);
}
command_candidates("zot")
.into_iter()
.find_map(|candidate| command_output(&candidate, &["--version"]))
}
fn detect_package_manager() -> Option<String> { fn detect_package_manager() -> Option<String> {
if command_output("pkg", &["--version"]).is_some() { if command_output("pkg", &["--version"]).is_some() {
return Some("pkg".to_string()); return Some("pkg".to_string());
@ -180,6 +199,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
node: command_output("node", &["--version"]), node: command_output("node", &["--version"]),
npm: command_output("npm", &["--version"]), npm: command_output("npm", &["--version"]),
pi: detect_pi_version(), pi: detect_pi_version(),
zot: detect_zot_version(),
npm_prefix: command_output("npm", &["config", "get", "prefix"]), npm_prefix: command_output("npm", &["config", "get", "prefix"]),
package_manager: detect_package_manager(), package_manager: detect_package_manager(),
iso_npm_globals_pin: BTreeMap::new(), iso_npm_globals_pin: BTreeMap::new(),