use std::path::{Path, PathBuf};
use std::process::Command;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{DiscoveryQuery, HarnessHomes, HarnessId};
pub const CODEX_BIN_ENV: &str = "SUPERCODE_CODEX_BIN";
pub const HERMES_BIN_ENV: &str = "SUPERCODE_HERMES_BIN";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionVerb {
New,
Reset,
Archive,
Delete,
}
impl SessionVerb {
pub const fn as_str(self) -> &'static str {
match self {
Self::New => "new",
Self::Reset => "reset",
Self::Archive => "archive",
Self::Delete => "delete",
}
}
pub const fn method(self) -> &'static str {
match self {
Self::New => "harness.v1.sessions.new",
Self::Reset => "harness.v1.sessions.reset",
Self::Archive => "harness.v1.sessions.archive",
Self::Delete => "harness.v1.sessions.delete",
}
}
const fn needs_session(self) -> bool {
!matches!(self, Self::New)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionDoor {
Cli,
Http,
Live(&'static str),
Store,
Daemon,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionMutation {
pub harness: String,
#[serde(default)]
pub session: Option<String>,
#[serde(default)]
pub cwd: Option<PathBuf>,
#[serde(default)]
pub connection: Option<String>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub bearer: Option<String>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub surface: Option<String>,
#[serde(default)]
pub homes: HarnessHomes,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionMutationOutcome {
pub harness: String,
pub verb: String,
pub ran: String,
pub session: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub row: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub archived: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionControlError {
Unsupported(String),
Invalid(String),
Failed(String),
}
impl std::fmt::Display for SessionControlError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unsupported(message) | Self::Invalid(message) | Self::Failed(message) => {
formatter.write_str(message)
}
}
}
}
impl std::error::Error for SessionControlError {}
type Result<T> = std::result::Result<T, SessionControlError>;
pub const CONTROLLED_SESSION_HARNESSES: &[&str] = &[
HarnessId::CODEX,
HarnessId::OPENCODE,
HarnessId::HERMES,
HarnessId::OPENCLAW,
HarnessId::ORCHESTRATOR,
HarnessId::SUPERCODE,
];
const REGISTERED_HARNESSES: &[&str] = &[
HarnessId::CLAUDE_CODE,
HarnessId::CODEX,
HarnessId::PI,
HarnessId::OPENCODE,
HarnessId::GROK,
HarnessId::GEMINI,
HarnessId::GOOSE,
HarnessId::HERMES,
HarnessId::OPENCLAW,
HarnessId::ORCHESTRATOR,
HarnessId::SUPERCODE,
];
pub fn supports_session_control(harness: &str) -> bool {
CONTROLLED_SESSION_HARNESSES.contains(&harness)
}
pub const ALL_SESSION_VERBS: [SessionVerb; 4] = [
SessionVerb::New,
SessionVerb::Reset,
SessionVerb::Archive,
SessionVerb::Delete,
];
pub fn controlled_verbs(harness: &str) -> Vec<&'static str> {
ALL_SESSION_VERBS
.into_iter()
.filter(|verb| door(harness, *verb).is_ok())
.map(SessionVerb::as_str)
.collect()
}
pub fn controlled_methods(harness: &str) -> Vec<&'static str> {
ALL_SESSION_VERBS
.into_iter()
.filter(|verb| door(harness, *verb).is_ok())
.map(SessionVerb::method)
.collect()
}
pub fn door(harness: &str, verb: SessionVerb) -> Result<SessionDoor> {
match (harness, verb) {
(HarnessId::CODEX, SessionVerb::Archive | SessionVerb::Delete) => Ok(SessionDoor::Cli),
(HarnessId::OPENCODE, SessionVerb::Archive | SessionVerb::Delete) => Ok(SessionDoor::Http),
(HarnessId::OPENCLAW, SessionVerb::New) => Ok(SessionDoor::Live("/new")),
(HarnessId::HERMES | HarnessId::OPENCLAW, SessionVerb::Reset) => {
Ok(SessionDoor::Live("/reset"))
}
(HarnessId::HERMES, SessionVerb::New) => Err(SessionControlError::Unsupported(
"hermes's ACP door advertises help, model, tools, context, reset, compress, steer, \
queue and version; `/new` is a GATEWAY command \
(`gateway/slash_commands.py::_handle_reset_command`) and hermes's ACP adapter sends \
any UNRECOGNIZED `/word` to the model as prose. Typing `/new` there would be a \
silent no-op dressed as a chat turn, so supercode refuses. `sessions.reset` IS \
advertised on that door and is supported"
.into(),
)),
(HarnessId::HERMES, SessionVerb::Delete) => Ok(SessionDoor::Cli),
(HarnessId::HERMES, SessionVerb::Archive) => Err(SessionControlError::Unsupported(
"hermes 0.21.0 registers `hermes sessions archive`, but it is a BULK filter verb \
(--older-than / --title / --cwd / ...) with no per-session selector, so archiving \
ONE conversation cannot be expressed through it. `sessions.delete` is per-session \
and is supported"
.into(),
)),
(HarnessId::OPENCLAW, SessionVerb::Archive | SessionVerb::Delete) => {
Err(SessionControlError::Unsupported(format!(
"openclaw v2026.7.1-2 registers `sessions list | cleanup | tail | \
export-trajectory | compact` and no `archive` or `delete`, so supercode refuses \
`sessions.{}` rather than inventing store-maintenance semantics for it",
verb.as_str()
)))
}
(HarnessId::SUPERCODE, SessionVerb::Archive | SessionVerb::Delete) => {
Ok(SessionDoor::Store)
}
(HarnessId::CLAUDE_CODE, SessionVerb::Archive | SessionVerb::Delete) => {
Err(SessionControlError::Unsupported(format!(
"claude-code publishes no conversation lifecycle verb: its sessions are removed \
by a RETENTION WINDOW the harness itself owns (`cleanupPeriodDays`), so \
supercode refuses `sessions.{}` rather than deleting files behind the \
harness's back",
verb.as_str()
)))
}
(HarnessId::ORCHESTRATOR, SessionVerb::New | SessionVerb::Reset) => Ok(SessionDoor::Daemon),
(HarnessId::ORCHESTRATOR, verb) => Err(SessionControlError::Unsupported(format!(
"the orchestrator's conversations are BINDINGS its daemon holds \
(`docs/ORCHESTRATOR-IR.md` §2.5): a binding is never archived or deleted — it \
ENDS, and the transcript belongs to the WORKER harness it addresses, which is \
where `sessions.{}` is performed. `sessions.new` and `sessions.reset` end a \
binding through the daemon's own operator door and are supported",
verb.as_str()
))),
(other, verb) if !REGISTERED_HARNESSES.contains(&other) => {
Err(SessionControlError::Unsupported(format!(
"`{other}` is not a registered harness, so `sessions.{}` has no door to go \
through",
verb.as_str()
)))
}
(_, SessionVerb::New) => Err(SessionControlError::Unsupported(format!(
"`{harness}` opens a conversation through `harness.v1.runtimes.start` (CLI: \
`supercode run --harness {harness}`), not through a slash command; `sessions.new` \
is only for the gateway harnesses whose surface outlives the conversation"
))),
(_, SessionVerb::Reset) => Err(SessionControlError::Unsupported(format!(
"`{harness}` has no conversation reset verb: a fresh conversation is a new runtime \
(`harness.v1.runtimes.start`). `sessions.reset` is only for the gateway harnesses \
whose surface outlives the conversation"
))),
(other, verb) => Err(SessionControlError::Unsupported(format!(
"`{other}` publishes no door for `sessions.{}`; conversation mutation is supported \
for: {}",
verb.as_str(),
CONTROLLED_SESSION_HARNESSES.join(", ")
))),
}
}
fn shell_quote(value: &str) -> String {
if !value.is_empty()
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_./:@=+,".contains(c))
{
return value.to_string();
}
format!("'{}'", value.replace('\'', "'\\''"))
}
#[derive(Debug, Clone)]
struct HarnessCommand {
program: String,
args: Vec<String>,
env: Vec<(String, String)>,
}
impl HarnessCommand {
fn new(program: impl Into<String>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
env: Vec::new(),
}
}
fn args<I: IntoIterator<Item = S>, S: Into<String>>(&mut self, values: I) -> &mut Self {
for value in values {
self.args.push(value.into());
}
self
}
fn env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.env.push((key.into(), value.into()));
self
}
fn narrate(&self) -> String {
let mut line = shell_quote(&self.program);
for arg in &self.args {
line.push(' ');
line.push_str(&shell_quote(arg));
}
line
}
fn run(&self) -> Result<String> {
let mut command = Command::new(&self.program);
command.args(&self.args);
for (key, value) in &self.env {
command.env(key, value);
}
command.stdin(std::process::Stdio::null());
let output = command.output().map_err(|error| {
SessionControlError::Failed(format!(
"`{}` could not be executed: {error}",
self.narrate()
))
})?;
if output.status.success() {
return Ok(String::from_utf8_lossy(&output.stdout).into_owned());
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let detail = if stderr.is_empty() { stdout } else { stderr };
Err(SessionControlError::Failed(format!(
"`{}` failed ({}): {}",
self.narrate(),
output.status,
if detail.is_empty() {
"the harness printed nothing".to_string()
} else {
detail
}
)))
}
}
pub fn harness_program(harness: &str) -> Result<String> {
let variable = match harness {
HarnessId::CODEX => CODEX_BIN_ENV,
HarnessId::HERMES => HERMES_BIN_ENV,
other => {
return Err(SessionControlError::Unsupported(format!(
"`{other}` has no conversation CLI supercode calls"
)));
}
};
if let Some(over) = std::env::var_os(variable) {
let over = over.to_string_lossy().trim().to_string();
if !over.is_empty() {
return Ok(over);
}
}
let program = crate::harness_support(harness)
.and_then(|descriptor| descriptor.runtime.default_launch)
.map(|launch| launch.program)
.ok_or_else(|| {
SessionControlError::Unsupported(format!(
"the registry has no launch for `{harness}`, so its CLI cannot be located"
))
})?;
Ok(program.strip_suffix("-acp").unwrap_or(&program).to_string())
}
fn hermes_home(mutation: &SessionMutation) -> PathBuf {
let root = mutation
.homes
.hermes
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
match mutation.profile.as_deref() {
Some(profile) => root.join("profiles").join(profile),
None => root,
}
}
fn codex_home(mutation: &SessionMutation) -> PathBuf {
let root = &mutation.homes.codex;
if root.file_name().is_some_and(|name| name == "sessions") {
return root
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
}
root.clone()
}
fn read_back(mutation: &SessionMutation, session: &str) -> Result<Option<Value>> {
if mutation.harness == HarnessId::SUPERCODE {
let store = crate::SessionStore::open(&mutation.homes.supercode).map_err(|error| {
SessionControlError::Failed(format!("supercode's session store is unreadable: {error}"))
})?;
return Ok(store
.list()
.into_iter()
.find(|info| info.name == session)
.map(|info| serde_json::to_value(info).unwrap_or(Value::Null)));
}
let page = crate::discover_session_page(&DiscoveryQuery {
harnesses: vec![HarnessId::new(mutation.harness.clone())],
homes: mutation.homes.clone(),
include_child_sessions: true,
..DiscoveryQuery::default()
})
.map_err(|error| {
SessionControlError::Failed(format!(
"the {} conversation store could not be re-read: {error}",
mutation.harness
))
})?;
Ok(page
.sessions
.into_iter()
.find(|descriptor| descriptor.locator.session_id == session)
.map(|descriptor| serde_json::to_value(descriptor).unwrap_or(Value::Null)))
}
pub async fn mutate(
verb: SessionVerb,
mutation: &SessionMutation,
) -> Result<SessionMutationOutcome> {
let door = door(&mutation.harness, verb)?;
let session = mutation.session.as_deref().unwrap_or("").trim().to_string();
if verb.needs_session() && session.is_empty() && !matches!(door, SessionDoor::Daemon) {
return Err(SessionControlError::Invalid(format!(
"`sessions.{}` needs the conversation to act on",
verb.as_str()
)));
}
match door {
SessionDoor::Live(command) => Err(SessionControlError::Invalid(format!(
"`{}` performs `sessions.{}` by typing `{command}` into a LIVE driven session; call \
it with an open runtime `connection`",
mutation.harness,
verb.as_str()
))),
SessionDoor::Cli => {
let command = cli_command(verb, mutation, &session)?;
let ran = command.narrate();
command.run()?;
let row = read_back(mutation, &session)?;
finish(verb, mutation, session, ran, row)
}
SessionDoor::Store => {
let store = crate::SessionStore::open(&mutation.homes.supercode).map_err(|error| {
SessionControlError::Failed(format!(
"supercode's session store is unreadable: {error}"
))
})?;
let ran = format!(
"supercode store {} {}",
verb.as_str(),
shell_quote(&session)
);
match verb {
SessionVerb::Archive => store.archive(&session),
SessionVerb::Delete => store.delete(&session),
_ => unreachable!("the door table only routes archive/delete to the store"),
}
.map_err(|error| SessionControlError::Failed(format!("`{ran}` failed: {error}")))?;
let row = read_back(mutation, &session)?;
finish(verb, mutation, session, ran, row)
}
SessionDoor::Daemon => orchestrator_mutate(verb, mutation),
SessionDoor::Http => {
let ran = opencode_call(verb, mutation, &session).await?;
let row = opencode_read_back(mutation, &session).await?;
finish(verb, mutation, session, ran, row)
}
}
}
fn orchestrator_mutate(
verb: SessionVerb,
mutation: &SessionMutation,
) -> Result<SessionMutationOutcome> {
let surface = mutation
.surface
.as_deref()
.map(str::trim)
.filter(|surface| !surface.is_empty())
.ok_or_else(|| {
SessionControlError::Invalid(format!(
"an orchestrator conversation is a BINDING on a surface, not a store row: \
`sessions.{}` needs `--surface \
<platform|chat_type|chat_id|thread_id|participant_id>` \
(`supercode sessions list --harness orchestrator` prints the surface of every \
binding)",
verb.as_str()
))
})?;
let root = mutation.homes.orchestrator.clone();
let profile = mutation
.profile
.as_deref()
.map(str::trim)
.filter(|profile| !profile.is_empty())
.unwrap_or("default");
let op = match verb {
SessionVerb::New => "sessions.new",
SessionVerb::Reset => "sessions.reset",
other => {
return Err(SessionControlError::Unsupported(format!(
"the orchestrator has no door for `sessions.{}`",
other.as_str()
)))
}
};
let args = serde_json::json!({ "surface": surface });
let answer = crate::orchestrator_door::call(&root, op, &args, profile).map_err(|error| {
match error {
crate::orchestrator_door::DoorError::Refused(message) => {
SessionControlError::Failed(message)
}
crate::orchestrator_door::DoorError::Failed(message) => {
SessionControlError::Failed(message)
}
}
})?;
let ran = format!("{} [{}]", answer.ran, answer.door.as_str());
let session = answer
.result
.pointer("/binding/session_id")
.and_then(Value::as_str)
.filter(|id| !id.is_empty())
.unwrap_or(surface)
.to_string();
let row = orchestrator_read_back(mutation, surface, &ran)?;
Ok(SessionMutationOutcome {
harness: mutation.harness.clone(),
verb: verb.as_str().to_string(),
ran,
session,
row,
archived: None,
deleted: None,
})
}
fn orchestrator_read_back(
mutation: &SessionMutation,
surface: &str,
ran: &str,
) -> Result<Option<Value>> {
let page = crate::discover_session_page(&DiscoveryQuery {
harnesses: vec![HarnessId::new(mutation.harness.clone())],
homes: mutation.homes.clone(),
include_child_sessions: true,
..DiscoveryQuery::default()
})
.map_err(|error| {
SessionControlError::Failed(format!(
"`{ran}` succeeded but the orchestrator's binding store could not be re-read: {error}"
))
})?;
let wanted = surface_columns(surface);
let mut best: Option<Value> = None;
let mut best_at = 0;
for descriptor in page.sessions {
let key = descriptor.nouns.surface.as_ref();
let found = [
key.and_then(|k| k.platform.clone()).unwrap_or_default(),
key.and_then(|k| k.kind.clone()).unwrap_or_default(),
key.and_then(|k| k.chat_id.clone()).unwrap_or_default(),
key.and_then(|k| k.thread_id.clone()).unwrap_or_default(),
key.and_then(|k| k.participant_id.clone())
.unwrap_or_default(),
];
if found != wanted {
continue;
}
let at = descriptor.updated_at_ms.unwrap_or_default();
if best.is_none() || at >= best_at {
best_at = at;
best = Some(serde_json::to_value(&descriptor).unwrap_or(Value::Null));
}
}
Ok(best)
}
fn surface_columns(surface: &str) -> [String; 5] {
let mut parts = surface.split('|');
std::array::from_fn(|_| parts.next().unwrap_or("").to_string())
}
fn finish(
verb: SessionVerb,
mutation: &SessionMutation,
session: String,
ran: String,
row: Option<Value>,
) -> Result<SessionMutationOutcome> {
let outcome = SessionMutationOutcome {
harness: mutation.harness.clone(),
verb: verb.as_str().to_string(),
ran: ran.clone(),
session: session.clone(),
row: row.clone(),
archived: None,
deleted: None,
};
match verb {
SessionVerb::Delete => {
if row.is_some() {
return Err(SessionControlError::Failed(format!(
"`{ran}` reported success but `{session}` is still in {}'s conversation store",
mutation.harness
)));
}
Ok(SessionMutationOutcome {
row: None,
deleted: Some(true),
..outcome
})
}
SessionVerb::Archive => {
if !archive_took_effect(mutation, row.as_ref()) {
return Err(SessionControlError::Failed(format!(
"`{ran}` reported success but {}'s store still lists `{session}` as an \
active conversation",
mutation.harness
)));
}
Ok(SessionMutationOutcome {
archived: Some(true),
..outcome
})
}
SessionVerb::New | SessionVerb::Reset => Ok(outcome),
}
}
fn archive_took_effect(mutation: &SessionMutation, row: Option<&Value>) -> bool {
let Some(row) = row else {
return true;
};
if mutation.harness == HarnessId::SUPERCODE {
return row
.get("archived")
.and_then(Value::as_bool)
.unwrap_or(false);
}
row.pointer("/time/archived")
.is_some_and(|value| !value.is_null())
}
fn cli_command(
verb: SessionVerb,
mutation: &SessionMutation,
session: &str,
) -> Result<HarnessCommand> {
match (mutation.harness.as_str(), verb) {
(HarnessId::CODEX, SessionVerb::Archive | SessionVerb::Delete) => {
let mut command = HarnessCommand::new(harness_program(HarnessId::CODEX)?);
command.env("CODEX_HOME", codex_home(mutation).to_string_lossy());
command.args([verb.as_str(), session]);
if matches!(verb, SessionVerb::Delete) {
command.args(["--force"]);
}
Ok(command)
}
(HarnessId::HERMES, SessionVerb::Delete) => {
let mut command = HarnessCommand::new(harness_program(HarnessId::HERMES)?);
command.env("HERMES_HOME", hermes_home(mutation).to_string_lossy());
command.args(["sessions", "delete", session, "--yes"]);
Ok(command)
}
(harness, verb) => Err(SessionControlError::Unsupported(format!(
"`{harness}` has no CLI verb for `sessions.{}`",
verb.as_str()
))),
}
}
fn opencode_endpoint(mutation: &SessionMutation) -> Result<(String, reqwest::Client)> {
let base = mutation
.base_url
.as_deref()
.map(|url| url.trim_end_matches('/').to_string())
.ok_or_else(|| {
SessionControlError::Invalid(
"opencode conversations are mutated through its own running server: pass \
`base_url` (the address `runtimes.start` reports, or an `opencode serve` you \
already run)"
.into(),
)
})?;
let mut headers = reqwest::header::HeaderMap::new();
if let Some(bearer) = mutation.bearer.as_deref().filter(|t| !t.trim().is_empty()) {
let mut value = reqwest::header::HeaderValue::from_str(&format!("Bearer {bearer}"))
.map_err(|_| {
SessionControlError::Invalid(
"the opencode bearer token is not a valid header value".into(),
)
})?;
value.set_sensitive(true);
headers.insert(reqwest::header::AUTHORIZATION, value);
}
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.map_err(|error| {
SessionControlError::Failed(format!("could not build the HTTP client: {error}"))
})?;
Ok((base, client))
}
async fn opencode_read_back(mutation: &SessionMutation, session: &str) -> Result<Option<Value>> {
let (base, client) = opencode_endpoint(mutation)?;
let url = format!("{base}/session/{session}");
let mut request = client.get(&url);
if let Some(cwd) = mutation.cwd.as_ref() {
request = request.query(&[("directory", cwd.to_string_lossy().into_owned())]);
}
let response = request.send().await.map_err(|error| {
SessionControlError::Failed(format!("`GET {url}` could not be sent: {error}"))
})?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(None);
}
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(SessionControlError::Failed(format!(
"`GET {url}` failed ({status}): {}",
body.trim()
)));
}
response
.json::<Value>()
.await
.map(|value| if value.is_null() { None } else { Some(value) })
.map_err(|error| {
SessionControlError::Failed(format!("`GET {url}` returned unreadable JSON: {error}"))
})
}
async fn opencode_call(
verb: SessionVerb,
mutation: &SessionMutation,
session: &str,
) -> Result<String> {
let (base, client) = opencode_endpoint(mutation)?;
let url = format!("{base}/session/{session}");
let directory = mutation
.cwd
.as_ref()
.map(|cwd| cwd.to_string_lossy().into_owned());
let (ran, request) = match verb {
SessionVerb::Delete => (format!("DELETE {url}"), client.delete(&url)),
SessionVerb::Archive => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.as_millis() as u64)
.unwrap_or_default();
(
format!("PATCH {url} {{\"time\":{{\"archived\":{now}}}}}"),
client
.patch(&url)
.json(&serde_json::json!({"time": {"archived": now}})),
)
}
other => {
return Err(SessionControlError::Unsupported(format!(
"opencode has no HTTP door for `sessions.{}`",
other.as_str()
)));
}
};
let request = match &directory {
Some(directory) => request.query(&[("directory", directory)]),
None => request,
};
let response = request.send().await.map_err(|error| {
SessionControlError::Failed(format!("`{ran}` could not be sent: {error}"))
})?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(SessionControlError::Failed(format!(
"`{ran}` failed ({status}): {}",
if body.trim().is_empty() {
"the server returned no body".to_string()
} else {
body.trim().to_string()
}
)));
}
Ok(ran)
}
pub fn live_outcome(
verb: SessionVerb,
mutation: &SessionMutation,
command: &str,
session: String,
) -> Result<SessionMutationOutcome> {
let row = read_back(mutation, &session).unwrap_or(None);
Ok(SessionMutationOutcome {
harness: mutation.harness.clone(),
verb: verb.as_str().to_string(),
ran: format!("{} live session: {command}", mutation.harness),
session,
row,
archived: None,
deleted: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_door_table_names_one_door_per_supported_pair() {
assert_eq!(
door(HarnessId::CODEX, SessionVerb::Archive).unwrap(),
SessionDoor::Cli
);
assert_eq!(
door(HarnessId::OPENCODE, SessionVerb::Delete).unwrap(),
SessionDoor::Http
);
assert_eq!(
door(HarnessId::OPENCLAW, SessionVerb::New).unwrap(),
SessionDoor::Live("/new")
);
assert_eq!(
door(HarnessId::OPENCLAW, SessionVerb::Reset).unwrap(),
SessionDoor::Live("/reset")
);
assert_eq!(
door(HarnessId::HERMES, SessionVerb::Reset).unwrap(),
SessionDoor::Live("/reset")
);
assert_eq!(
door(HarnessId::HERMES, SessionVerb::Delete).unwrap(),
SessionDoor::Cli
);
assert_eq!(
door(HarnessId::SUPERCODE, SessionVerb::Archive).unwrap(),
SessionDoor::Store
);
}
#[test]
fn every_refusal_names_the_reason_and_never_a_silent_no_op() {
for (harness, verb, needle) in [
(HarnessId::HERMES, SessionVerb::Archive, "BULK filter verb"),
(
HarnessId::HERMES,
SessionVerb::New,
"sends any UNRECOGNIZED `/word` to the model as prose",
),
(HarnessId::OPENCLAW, SessionVerb::Delete, "v2026.7.1-2"),
(
HarnessId::CLAUDE_CODE,
SessionVerb::Delete,
"RETENTION WINDOW",
),
(
HarnessId::CLAUDE_CODE,
SessionVerb::New,
"harness.v1.runtimes.start",
),
(
HarnessId::CODEX,
SessionVerb::New,
"harness.v1.runtimes.start",
),
(
HarnessId::SUPERCODE,
SessionVerb::Reset,
"no conversation reset verb",
),
(
HarnessId::ORCHESTRATOR,
SessionVerb::Archive,
"a binding is never archived or deleted",
),
] {
let error = door(harness, verb).unwrap_err();
assert!(
matches!(error, SessionControlError::Unsupported(_)),
"{harness}.{}: {error}",
verb.as_str()
);
assert!(
error.to_string().contains(needle),
"{harness}.{} must explain itself, got: {error}",
verb.as_str()
);
}
}
#[test]
fn controlled_verbs_track_the_door_table() {
assert_eq!(
controlled_verbs(HarnessId::CODEX),
vec!["archive", "delete"]
);
assert_eq!(controlled_verbs(HarnessId::HERMES), vec!["reset", "delete"]);
assert_eq!(controlled_verbs(HarnessId::OPENCLAW), vec!["new", "reset"]);
assert_eq!(
controlled_verbs(HarnessId::OPENCODE),
vec!["archive", "delete"]
);
assert_eq!(
controlled_verbs(HarnessId::SUPERCODE),
vec!["archive", "delete"]
);
assert!(controlled_verbs(HarnessId::CLAUDE_CODE).is_empty());
assert!(controlled_verbs(HarnessId::PI).is_empty());
assert_eq!(
controlled_verbs(HarnessId::ORCHESTRATOR),
vec!["new", "reset"]
);
assert_eq!(
door(HarnessId::ORCHESTRATOR, SessionVerb::Reset).unwrap(),
SessionDoor::Daemon
);
assert!(controlled_verbs("not-a-harness").is_empty());
for harness in crate::harness_support_registry().harnesses {
assert_eq!(
!controlled_verbs(harness.id.as_str()).is_empty(),
supports_session_control(harness.id.as_str()),
"{}: CONTROLLED_SESSION_HARNESSES must track the door table",
harness.id.as_str()
);
}
}
#[test]
fn the_registered_harness_list_matches_the_compiled_registry() {
let mut from_registry: Vec<String> = crate::harness_support_registry()
.harnesses
.into_iter()
.map(|descriptor| descriptor.id.as_str().to_string())
.collect();
from_registry.sort();
let mut declared: Vec<String> = REGISTERED_HARNESSES
.iter()
.map(|id| id.to_string())
.collect();
declared.sort();
assert_eq!(declared, from_registry);
}
#[test]
fn codex_home_is_the_parent_of_the_sessions_root() {
let mutation = SessionMutation {
harness: HarnessId::CODEX.into(),
homes: HarnessHomes {
codex: PathBuf::from("/tmp/iso/.codex/sessions"),
..HarnessHomes::default()
},
..SessionMutation::default()
};
assert_eq!(codex_home(&mutation), PathBuf::from("/tmp/iso/.codex"));
}
#[test]
fn a_hermes_profile_is_a_full_home() {
let mutation = SessionMutation {
harness: HarnessId::HERMES.into(),
profile: Some("work".into()),
homes: HarnessHomes {
hermes: PathBuf::from("/tmp/iso/.hermes/state.db"),
..HarnessHomes::default()
},
..SessionMutation::default()
};
assert_eq!(
hermes_home(&mutation),
PathBuf::from("/tmp/iso/.hermes/profiles/work")
);
}
#[tokio::test]
async fn a_live_door_asked_for_out_of_band_says_so() {
let error = mutate(
SessionVerb::Reset,
&SessionMutation {
harness: HarnessId::HERMES.into(),
session: Some("s1".into()),
..SessionMutation::default()
},
)
.await
.unwrap_err();
assert!(matches!(error, SessionControlError::Invalid(_)));
assert!(error.to_string().contains("/reset"));
assert!(error.to_string().contains("connection"));
}
#[tokio::test]
async fn opencode_refuses_to_guess_an_endpoint() {
let error = mutate(
SessionVerb::Delete,
&SessionMutation {
harness: HarnessId::OPENCODE.into(),
session: Some("ses_1".into()),
..SessionMutation::default()
},
)
.await
.unwrap_err();
assert!(matches!(error, SessionControlError::Invalid(_)));
assert!(error.to_string().contains("base_url"));
}
#[tokio::test]
async fn a_verb_without_its_conversation_is_invalid() {
let error = mutate(
SessionVerb::Delete,
&SessionMutation {
harness: HarnessId::CODEX.into(),
..SessionMutation::default()
},
)
.await
.unwrap_err();
assert!(matches!(error, SessionControlError::Invalid(_)));
assert!(error.to_string().contains("sessions.delete"));
}
}