feat: add ecc2 session messaging primitives

This commit is contained in:
Affaan Mustafa
2026-04-07 12:13:47 -07:00
parent 1d46559201
commit 27b8272fad
5 changed files with 472 additions and 32 deletions

View File

@@ -50,6 +50,11 @@ enum Commands {
/// Session ID or alias
session_id: String,
},
/// Send or inspect inter-session messages
Messages {
#[command(subcommand)]
command: MessageCommands,
},
/// Run as background daemon
Daemon,
#[command(hide = true)]
@@ -65,6 +70,40 @@ enum Commands {
},
}
#[derive(clap::Subcommand, Debug)]
enum MessageCommands {
/// Send a structured message between sessions
Send {
#[arg(long)]
from: String,
#[arg(long)]
to: String,
#[arg(long, value_enum)]
kind: MessageKindArg,
#[arg(long)]
text: String,
#[arg(long)]
context: Option<String>,
#[arg(long)]
file: Vec<String>,
},
/// Show recent messages for a session
Inbox {
session_id: String,
#[arg(long, default_value_t = 10)]
limit: usize,
},
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum MessageKindArg {
Handoff,
Query,
Response,
Completed,
Conflict,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
@@ -108,6 +147,49 @@ async fn main() -> Result<()> {
let resumed_id = session::manager::resume_session(&db, &cfg, &session_id).await?;
println!("Session resumed: {resumed_id}");
}
Some(Commands::Messages { command }) => match command {
MessageCommands::Send {
from,
to,
kind,
text,
context,
file,
} => {
let from = resolve_session_id(&db, &from)?;
let to = resolve_session_id(&db, &to)?;
let message = build_message(kind, text, context, file)?;
comms::send(&db, &from, &to, &message)?;
println!("Message sent: {} -> {}", short_session(&from), short_session(&to));
}
MessageCommands::Inbox { session_id, limit } => {
let session_id = resolve_session_id(&db, &session_id)?;
let messages = db.list_messages_for_session(&session_id, limit)?;
let unread_before = db
.unread_message_counts()?
.get(&session_id)
.copied()
.unwrap_or(0);
if unread_before > 0 {
let _ = db.mark_messages_read(&session_id)?;
}
if messages.is_empty() {
println!("No messages for {}", short_session(&session_id));
} else {
println!("Messages for {}", short_session(&session_id));
for message in messages {
println!(
"{} {} -> {} | {}",
message.timestamp.format("%H:%M:%S"),
short_session(&message.from_session),
short_session(&message.to_session),
comms::preview(&message.msg_type, &message.content)
);
}
}
}
},
Some(Commands::Daemon) => {
println!("Starting ECC daemon...");
session::daemon::run(db, cfg).await?;
@@ -125,6 +207,53 @@ async fn main() -> Result<()> {
Ok(())
}
fn resolve_session_id(db: &session::store::StateStore, value: &str) -> Result<String> {
if value == "latest" {
return db
.get_latest_session()?
.map(|session| session.id)
.ok_or_else(|| anyhow::anyhow!("No sessions found"));
}
db.get_session(value)?
.map(|session| session.id)
.ok_or_else(|| anyhow::anyhow!("Session not found: {value}"))
}
fn build_message(
kind: MessageKindArg,
text: String,
context: Option<String>,
files: Vec<String>,
) -> Result<comms::MessageType> {
Ok(match kind {
MessageKindArg::Handoff => comms::MessageType::TaskHandoff {
task: text,
context: context.unwrap_or_default(),
},
MessageKindArg::Query => comms::MessageType::Query { question: text },
MessageKindArg::Response => comms::MessageType::Response { answer: text },
MessageKindArg::Completed => comms::MessageType::Completed {
summary: text,
files_changed: files,
},
MessageKindArg::Conflict => {
let file = files
.first()
.cloned()
.ok_or_else(|| anyhow::anyhow!("Conflict messages require at least one --file"))?;
comms::MessageType::Conflict {
file,
description: context.unwrap_or(text),
}
}
})
}
fn short_session(session_id: &str) -> String {
session_id.chars().take(8).collect()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -139,4 +268,41 @@ mod tests {
_ => panic!("expected resume subcommand"),
}
}
#[test]
fn cli_parses_messages_send_command() {
let cli = Cli::try_parse_from([
"ecc",
"messages",
"send",
"--from",
"planner",
"--to",
"worker",
"--kind",
"query",
"--text",
"Need context",
])
.expect("messages send should parse");
match cli.command {
Some(Commands::Messages {
command:
MessageCommands::Send {
from,
to,
kind,
text,
..
},
}) => {
assert_eq!(from, "planner");
assert_eq!(to, "worker");
assert!(matches!(kind, MessageKindArg::Query));
assert_eq!(text, "Need context");
}
_ => panic!("expected messages send subcommand"),
}
}
}