use std::collections::BTreeMap;
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use oneharness_core::domain::history::{parse_labels, HistoryId, HistoryLabels, HistoryPointer};
use oneharness_core::domain::usage::UtcInstant;
use oneharness_core::io::history::read_pointers;
use serde::Serialize;
use crate::error::{Error, Result};
use crate::ledger::RunPaths;
pub const HISTORY_ENV: &str = "ONEHARNESS_HISTORY";
pub const POINTER_FILE_ENV: &str = "ONEHARNESS_HISTORY_POINTER_FILE";
pub const LABELS_ENV: &str = "ONEHARNESS_HISTORY_LABELS";
pub const HISTORY_DIR_ENV: &str = "ONEHARNESS_HISTORY_DIR";
pub const SESSIONS_FILE: &str = "oneharness-sessions.jsonl";
pub const LABEL_PREFIX: &str = "onepipeline.";
pub const RUN_ID_LABEL: &str = "onepipeline.run_id";
pub const PROJECT_LABEL: &str = "onepipeline.project";
pub const SCOPE_LABEL: &str = "onepipeline.scope";
pub const NODE_LABEL: &str = "onepipeline.node";
pub const STEP_LABEL: &str = "onepipeline.step";
pub const ATTEMPT_LABEL: &str = "onepipeline.attempt";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Scope {
Node,
Observer,
PrAuthor,
}
impl Scope {
pub const ALL: [Scope; 3] = [Scope::Node, Scope::Observer, Scope::PrAuthor];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Scope::Node => "node",
Scope::Observer => "observer",
Scope::PrAuthor => "pr-author",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stamp<'a> {
pub run: &'a str,
pub project: Option<&'a str>,
pub launched: Launched<'a>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Launched<'a> {
Node {
node: &'a str,
step: Option<&'a str>,
attempt: NonZeroU32,
},
Observer,
PrAuthor {
node: &'a str,
attempt: NonZeroU32,
},
}
impl Launched<'_> {
#[must_use]
pub const fn scope(self) -> Scope {
match self {
Launched::Node { .. } => Scope::Node,
Launched::Observer => Scope::Observer,
Launched::PrAuthor { .. } => Scope::PrAuthor,
}
}
}
impl Stamp<'_> {
fn pairs(&self) -> Vec<(&'static str, String)> {
let mut pairs = vec![
(RUN_ID_LABEL, self.run.to_string()),
(SCOPE_LABEL, self.launched.scope().as_str().to_string()),
];
if let Some(project) = self.project {
pairs.push((PROJECT_LABEL, project.to_string()));
}
match self.launched {
Launched::Node {
node,
step,
attempt,
} => {
pairs.push((NODE_LABEL, node.to_string()));
if let Some(step) = step {
pairs.push((STEP_LABEL, step.to_string()));
}
pairs.push((ATTEMPT_LABEL, attempt.to_string()));
}
Launched::Observer => {}
Launched::PrAuthor { node, attempt } => {
pairs.push((NODE_LABEL, node.to_string()));
pairs.push((ATTEMPT_LABEL, attempt.to_string()));
}
}
pairs
}
}
pub(crate) fn inherited_labels() -> Result<Option<String>> {
match std::env::var_os(LABELS_ENV) {
None => Ok(None),
Some(value) => value.into_string().map(Some).map_err(|value| {
Error::Refused(format!(
"the launch inherits a {LABELS_ENV} that is not Unicode ({value:?}), which \
oneharness cannot read as a label set"
))
}),
}
}
pub(crate) fn overlay(
paths: &RunPaths,
inherited: Option<&str>,
stamp: &Stamp<'_>,
) -> Result<Vec<(String, String)>> {
let pointer_file = paths.oneharness_sessions();
let pointer_file = std::path::absolute(&pointer_file).map_err(|source| Error::Ledger {
path: pointer_file,
source,
})?;
Ok(vec![
(HISTORY_ENV.to_string(), "1".to_string()),
(
POINTER_FILE_ENV.to_string(),
pointer_file.display().to_string(),
),
(LABELS_ENV.to_string(), compose_labels(inherited, stamp)?),
])
}
pub fn compose_labels(inherited: Option<&str>, stamp: &Stamp<'_>) -> Result<String> {
let mut labels: BTreeMap<String, String> = match inherited.map(str::trim) {
Some(value) if !value.is_empty() => parse_labels(value.split(',').map(str::trim))
.map_err(|error| {
Error::Refused(format!(
"the launch inherits a {LABELS_ENV} that oneharness would refuse: {error}"
))
})?
.as_map()
.iter()
.filter(|(key, _)| !key.starts_with(LABEL_PREFIX))
.map(|(key, value)| (key.clone(), value.clone()))
.collect(),
_ => BTreeMap::new(),
};
for (key, value) in stamp.pairs() {
labels.insert(key.to_string(), value);
}
let labels = HistoryLabels::new(labels).map_err(|error| {
Error::Refused(format!(
"the dispatch cannot be stamped with a history label oneharness would refuse: {error}"
))
})?;
if let Some((key, _)) = labels
.as_map()
.iter()
.find(|(_, value)| value.contains(','))
{
return Err(Error::Refused(format!(
"the dispatch cannot be stamped: history label `{key}` carries a comma, which the \
{LABELS_ENV} wire format has no escape for"
)));
}
Ok(render_labels(labels.as_map()))
}
#[must_use]
pub fn render_labels(labels: &BTreeMap<String, String>) -> String {
labels
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>()
.join(",")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentScope<'a> {
Run,
Node(&'a str),
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
pub struct Agents {
pub sessions: Vec<AgentSession>,
pub skipped: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AgentSession {
pub history_session: String,
pub name: String,
pub history_dir: String,
pub history_project: String,
pub history_file: String,
pub project: String,
pub started: UtcInstant,
pub labels: HistoryLabels,
pub runs: Vec<AgentRun>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AgentRun {
pub history_id: HistoryId,
pub harness: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub variant: Option<String>,
pub harness_id: String,
pub started: UtcInstant,
}
impl Agents {
pub fn of_run(paths: &RunPaths, scope: AgentScope<'_>) -> Result<Self> {
let mut agents = Self::default();
agents.absorb(&paths.oneharness_sessions(), scope)?;
Ok(agents)
}
pub(crate) fn absorb(&mut self, pointer_file: &Path, scope: AgentScope<'_>) -> Result<()> {
let read = read_pointers(pointer_file).map_err(|error| {
Error::Invalid(format!(
"cannot read the run's pointer file {}: {error}",
pointer_file.display()
))
})?;
self.skipped += read.skipped;
for pointer in read
.pointers
.iter()
.filter(|pointer| selects(scope, pointer))
{
self.record(pointer);
}
Ok(())
}
fn record(&mut self, pointer: &HistoryPointer) {
let run = AgentRun {
history_id: pointer.history_id(),
harness: pointer.harness().to_string(),
variant: pointer.variant().map(str::to_string),
harness_id: pointer.harness_id().to_string(),
started: pointer.started().clone(),
};
if let Some(session) = self
.sessions
.iter_mut()
.find(|session| session.history_session == pointer.history_session())
{
if run.started < session.started {
session.started = run.started.clone();
}
session.runs.push(run);
return;
}
self.sessions.push(AgentSession {
history_session: pointer.history_session().to_string(),
name: pointer.name().to_string(),
history_dir: pointer.history_dir().to_string(),
history_project: pointer.history_project().to_string(),
history_file: pointer.history_file().to_string(),
project: pointer.project().to_string(),
started: run.started.clone(),
labels: pointer.labels().clone(),
runs: vec![run],
});
}
}
fn selects(scope: AgentScope<'_>, pointer: &HistoryPointer) -> bool {
match scope {
AgentScope::Run => true,
AgentScope::Node(node) => {
pointer
.labels()
.as_map()
.get(NODE_LABEL)
.map(String::as_str)
== Some(node)
}
}
}
#[must_use]
pub fn render(agents: &Agents) -> String {
let mut out = String::new();
if agents.sessions.is_empty() {
out.push_str("no sessions recorded\n");
}
for session in &agents.sessions {
out.push_str(&format!(
"{} {} started {}\n",
session.history_session, session.name, session.started
));
out.push_str(&format!(
" store {} project {}\n",
session.history_dir, session.history_project
));
out.push_str(&format!(" file {}\n", session.history_file));
out.push_str(&format!(" cwd {}\n", session.project));
if !session.labels.is_empty() {
out.push_str(&format!(
" labels {}\n",
render_labels(session.labels.as_map())
));
}
for run in &session.runs {
out.push_str(&format!(
" run {} {} started {}\n",
run.history_id, run.harness_id, run.started
));
}
}
if agents.skipped > 0 {
out.push_str(&format!(
"{} line(s) skipped: torn or not a pointer\n",
agents.skipped
));
}
out
}
impl RunPaths {
#[must_use]
pub fn oneharness_sessions(&self) -> PathBuf {
self.dir.join(SESSIONS_FILE)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn stamp<'a>(node: &'a str, step: Option<&'a str>) -> Stamp<'a> {
Stamp {
run: "demo-1",
project: Some("plans:demo"),
launched: Launched::Node {
node,
step,
attempt: NonZeroU32::MIN,
},
}
}
#[test]
fn inherited_labels_survive_except_under_the_engines_prefix() {
let composed = compose_labels(
Some("owner=ci, onepipeline.node=other,onepipeline.scope=observer,team=core"),
&stamp("build", None),
)
.expect("a well-formed inherited set composes");
assert_eq!(
composed,
"onepipeline.attempt=1,onepipeline.node=build,onepipeline.project=plans:demo,\
onepipeline.run_id=demo-1,onepipeline.scope=node,owner=ci,team=core"
);
for inherited in [None, Some(""), Some(" ")] {
let composed = compose_labels(inherited, &stamp("build", None)).expect("composes");
assert_eq!(
composed,
"onepipeline.attempt=1,onepipeline.node=build,onepipeline.project=plans:demo,\
onepipeline.run_id=demo-1,onepipeline.scope=node"
);
}
let observer = compose_labels(
None,
&Stamp {
run: "demo-1",
project: None,
launched: Launched::Observer,
},
)
.expect("composes");
assert_eq!(
observer,
"onepipeline.run_id=demo-1,onepipeline.scope=observer"
);
let step = compose_labels(None, &stamp("service", Some("implement"))).expect("composes");
assert!(step.contains("onepipeline.step=implement"), "{step}");
let drafting = compose_labels(
None,
&Stamp {
run: "demo-1",
project: None,
launched: Launched::PrAuthor {
node: "service",
attempt: NonZeroU32::MIN.saturating_add(1),
},
},
)
.expect("composes");
assert_eq!(
drafting,
"onepipeline.attempt=2,onepipeline.node=service,onepipeline.run_id=demo-1,\
onepipeline.scope=pr-author"
);
}
#[test]
fn a_value_the_grammar_or_the_wire_refuses_refuses_the_launch_naming_the_key() {
let comma = compose_labels(None, &stamp("a,b", None)).expect_err("a comma");
assert!(
comma.to_string().contains(NODE_LABEL) && comma.to_string().contains("comma"),
"{comma}"
);
let control = compose_labels(None, &stamp("a\u{7}b", None)).expect_err("a control");
assert!(control.to_string().contains(NODE_LABEL), "{control}");
let long = "x".repeat(257);
let too_long = compose_labels(None, &stamp(&long, None)).expect_err("too long");
assert!(too_long.to_string().contains(NODE_LABEL), "{too_long}");
let malformed =
compose_labels(Some("nokey"), &stamp("build", None)).expect_err("malformed");
assert!(malformed.to_string().contains(LABELS_ENV), "{malformed}");
}
#[test]
fn every_scope_has_a_word_of_its_own() {
let words: std::collections::BTreeSet<&str> =
Scope::ALL.iter().map(|scope| scope.as_str()).collect();
assert_eq!(words.len(), Scope::ALL.len());
assert_eq!(Scope::Node.as_str(), "node");
assert_eq!(Scope::Observer.as_str(), "observer");
assert_eq!(Scope::PrAuthor.as_str(), "pr-author");
}
#[test]
fn a_pointer_file_reads_grouped_by_session_and_a_missing_one_reads_empty() {
use oneharness_core::domain::harness::HarnessIdentity;
use oneharness_core::domain::history::{HistoryId, PointerSession};
use oneharness_core::domain::usage::UtcInstant;
let root = std::env::temp_dir().join(format!("onepipeline-agents-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let paths = RunPaths::under(&root, "demo-1");
std::fs::create_dir_all(&paths.dir).expect("a run root");
assert_eq!(
Agents::of_run(&paths, AgentScope::Run).expect("no file reads empty"),
Agents::default()
);
let store = if cfg!(windows) { "C:\\store" } else { "/store" };
let line = |session: &str, node: &str, id: u32, started: i64| {
let labels = HistoryLabels::new(BTreeMap::from([
(NODE_LABEL.to_string(), node.to_string()),
("owner".to_string(), "ci".to_string()),
]))
.expect("valid labels");
let session = PointerSession::new(
Path::new(store),
&Path::new(store)
.join("proj")
.join(format!("{session}.jsonl")),
"turn",
if cfg!(windows) { "C:\\work" } else { "/work" },
labels,
)
.expect("a session");
let identity: HarnessIdentity = "claude-code".parse().expect("an identity");
let history_id: HistoryId = format!("00000000-0000-4000-8000-{id:012}")
.parse()
.expect("a history id");
let pointer = HistoryPointer::new(
&session,
history_id,
&identity,
UtcInstant::from_epoch(started),
)
.expect("a pointer");
format!("{}\n", serde_json::to_string(&pointer).expect("serialises"))
};
let mut text = String::new();
text.push_str(&line("s-one", "build", 1, 200));
text.push_str("{\"not\": \"a pointer\"}\n");
text.push_str(&line("s-two", "test", 2, 150));
text.push_str(&line("s-one", "build", 3, 100));
text.push_str("{\"torn");
std::fs::write(paths.oneharness_sessions(), text).expect("the file is written");
let all = Agents::of_run(&paths, AgentScope::Run).expect("reads");
assert_eq!(all.skipped, 2, "{all:?}");
assert_eq!(all.sessions.len(), 2, "{all:?}");
let one = &all.sessions[0];
assert_eq!(one.history_session, "s-one");
assert_eq!(one.history_dir, store);
assert_eq!(one.history_project, "proj");
assert_eq!(one.labels.as_map()["owner"], "ci");
assert_eq!(
one.runs
.iter()
.map(|run| run.history_id.to_string())
.collect::<Vec<_>>(),
vec![
"00000000-0000-4000-8000-000000000001",
"00000000-0000-4000-8000-000000000003"
]
);
assert_eq!(one.started, UtcInstant::from_epoch(100));
assert_eq!(one.runs[0].harness_id, "claude-code");
let build = Agents::of_run(&paths, AgentScope::Node("build")).expect("reads");
assert_eq!(build.sessions.len(), 1);
assert_eq!(build.sessions[0].history_session, "s-one");
let none = Agents::of_run(&paths, AgentScope::Node("nope")).expect("reads");
assert!(none.sessions.is_empty());
assert_eq!(
none.skipped, 2,
"skipped lines are counted whatever the scope"
);
let rendered = render(&all);
assert!(rendered.starts_with("s-one turn started "), "{rendered}");
assert!(
rendered.contains(" labels onepipeline.node=build,owner=ci\n"),
"{rendered}"
);
assert!(rendered.contains("2 line(s) skipped"), "{rendered}");
assert_eq!(render(&Agents::default()), "no sessions recorded\n");
let _ = std::fs::remove_dir_all(&root);
}
}