use crate::a2a::types::*;
use crate::brain::agent::service::notify_policy::{
CONFIRM_CAP, DeliveryMode, confirm_route, resolve_mode, validate_sender_label,
};
use crate::brain::agent::service::notify_receipts;
use crate::brain::agent::service::quiet_delivery;
use crate::brain::agent::service::session_routes::Delivery;
use crate::brain::agent::service::session_routes::deliver_to_session;
use crate::brain::agent::{PushOrigin, QueuedUserMessage};
use crate::services::{ServiceContext, SessionService};
pub(crate) const CLI_SENDER_PREFIX: &str = "cli:";
pub(crate) const DEFAULT_CLI_SENDER_LABEL: &str = "CLI tooling";
pub async fn handle_session_notify(
req_id: serde_json::Value,
params: serde_json::Value,
service_context: ServiceContext,
) -> JsonRpcResponse {
let session_id = match params.get("session_id").and_then(serde_json::Value::as_str) {
Some(raw) => match raw.parse::<uuid::Uuid>() {
Ok(id) => id,
Err(_) => {
return JsonRpcResponse::error(
req_id,
error_codes::INVALID_PARAMS,
format!("'session_id' is not a valid UUID: {raw}"),
);
}
},
None => {
return JsonRpcResponse::error(
req_id,
error_codes::INVALID_PARAMS,
"'session_id' is required",
);
}
};
let message = match params.get("message").and_then(serde_json::Value::as_str) {
Some(m) if !m.trim().is_empty() => m.to_string(),
_ => {
return JsonRpcResponse::error(
req_id,
error_codes::INVALID_PARAMS,
"'message' is required and must be non-empty",
);
}
};
let title = params
.get("title")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|t| !t.is_empty())
.map(str::to_string);
let sender = match params.get("sender").and_then(serde_json::Value::as_str) {
Some(raw) => {
let label = raw.trim();
if label.is_empty() {
DEFAULT_CLI_SENDER_LABEL.to_string()
} else if let Err(e) = validate_sender_label(label) {
return JsonRpcResponse::error(req_id, error_codes::INVALID_PARAMS, e);
} else {
label.to_string()
}
}
None => DEFAULT_CLI_SENDER_LABEL.to_string(),
};
let mode = match resolve_mode(
params
.get("delivery")
.and_then(|d| d.get("mode"))
.and_then(serde_json::Value::as_str),
params.get("interrupt").and_then(serde_json::Value::as_bool),
params.get("delivery"),
) {
Ok(m) => m,
Err(e) => {
return JsonRpcResponse::error(req_id, error_codes::INVALID_PARAMS, e);
}
};
let session_svc = SessionService::new(service_context);
match session_svc.get_session(session_id).await {
Ok(Some(_session)) => {}
Ok(None) => {
return JsonRpcResponse::success(
req_id,
serde_json::json!({
"outcome": "no_route",
"detail": format!(
"session {session_id} does not exist — nothing sent, nothing created"
),
}),
);
}
Err(e) => {
return JsonRpcResponse::error(
req_id,
error_codes::INTERNAL_ERROR,
format!("session lookup failed: {e}"),
);
}
}
let header = match &title {
Some(t) => format!("📨 {t} (from {sender}):"),
None => format!("📨 notify from {sender}:"),
};
let msg = QueuedUserMessage {
context_text: format!("[session-notify from={CLI_SENDER_PREFIX}{sender}]\n\n{message}"),
display_text: format!("{header}\n{message}"),
origin: PushOrigin::SessionNotify,
bg_meta: None,
};
if let DeliveryMode::Quiet {
quiet_for,
max_delay,
} = mode
{
let id = quiet_delivery::defer_quiet(session_id, msg, quiet_for, max_delay);
notify_receipts::record_queued(id, session_id);
return JsonRpcResponse::success(
req_id,
serde_json::json!({
"outcome": "deferred",
"detail": format!(
"deferred for session {session_id}: delivers once the session has been \
quiet for {}s (hard cap {}s) — notification id {id}",
quiet_for.as_secs(),
max_delay.as_secs()
),
"notify_id": id.to_string(),
"notify_state": "deferred",
}),
);
}
let interrupt = matches!(mode, DeliveryMode::TurnEnd);
let confirm = params
.get("confirm")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let notify_id = uuid::Uuid::new_v4();
let (outcome, detail, extra) = match deliver_to_session(session_id, msg, interrupt) {
Delivery::Delivered => {
notify_receipts::record_queued(notify_id, session_id);
if confirm {
let (state, cdetail, reason) = confirm_route(session_id, CONFIRM_CAP).await;
(
"delivered",
cdetail,
serde_json::json!({
"notify_id": notify_id.to_string(),
"notify_state": state,
"notify_reason": reason,
}),
)
} else {
(
"delivered",
format!("delivered to session {session_id}"),
serde_json::json!({ "notify_id": notify_id.to_string() }),
)
}
}
Delivery::Redirected { to } => {
notify_receipts::record_queued(notify_id, to);
if confirm {
let (state, cdetail, reason) = confirm_route(to, CONFIRM_CAP).await;
(
"delivered",
format!("{cdetail} (redirected to session {to})"),
serde_json::json!({
"notify_id": notify_id.to_string(),
"notify_state": state,
"notify_reason": reason,
"notify_occupant": to.to_string(),
}),
)
} else {
(
"delivered",
format!(
"redirected to session {to}: session {session_id} no longer owns its \
channel (#19)"
),
serde_json::json!({
"notify_id": notify_id.to_string(),
"notify_occupant": to.to_string(),
}),
)
}
}
Delivery::Parked => {
notify_receipts::record_queued(notify_id, session_id);
(
"parked",
format!(
"queued for session {session_id}: its channel has not claimed it since \
the last restart (#1206) — it delivers on the next claim"
),
serde_json::json!({ "notify_id": notify_id.to_string() }),
)
}
Delivery::RefusedInFlight { redirected_to } => {
let who = redirected_to.map_or_else(
|| session_id.to_string(),
|to| format!("{to} (redirected from {session_id})"),
);
(
"refused_in_flight",
format!(
"session {who} is mid-turn and interrupt was not set — retry when \
idle or resend with interrupt=true (#13 failsafe)"
),
serde_json::json!({}),
)
}
Delivery::NoRoute => (
"no_route",
format!("no live route for session {session_id} and nothing is holding it"),
serde_json::json!({}),
),
};
JsonRpcResponse::success(req_id, {
let mut body = serde_json::json!({ "outcome": outcome, "detail": detail });
if let (Some(obj), Some(extra_obj)) = (body.as_object_mut(), extra.as_object()) {
for (k, v) in extra_obj {
obj.insert(k.clone(), v.clone());
}
}
body
})
}
pub fn handle_notify_status(
req_id: serde_json::Value,
params: serde_json::Value,
) -> JsonRpcResponse {
use crate::brain::agent::service::notify_receipts::{self, ReceiptState};
let raw = match params.get("notify_id").and_then(serde_json::Value::as_str) {
Some(raw) => raw,
None => {
return JsonRpcResponse::error(
req_id,
error_codes::INVALID_PARAMS,
"'notify_id' is required",
);
}
};
let id: uuid::Uuid = match raw.parse() {
Ok(id) => id,
Err(_) => {
return JsonRpcResponse::error(
req_id,
error_codes::INVALID_PARAMS,
format!("'notify_id' is not a valid UUID: {raw}"),
);
}
};
match notify_receipts::status(id) {
None => JsonRpcResponse::success(
req_id,
serde_json::json!({
"outcome": "unknown_id",
"detail": format!(
"no notification {id} is tracked in this process — receipts are \
in-memory and do not survive restarts"
),
"notify_id": id.to_string(),
"notify_state": "unknown_id",
}),
),
Some(receipt) => {
let (outcome, detail) = match receipt.state {
ReceiptState::Injected => {
let at = receipt
.injected_at
.map(|t| t.to_rfc3339())
.unwrap_or_default();
(
"injected",
format!(
"notification {id} was INJECTED into session {}'s model \
context at {at} — the receiving machinery consumed it",
receipt.target
),
)
}
ReceiptState::Queued => (
"queued",
format!(
"notification {id} is routed to session {} but NOT yet observed \
at a tool-loop drain point — delivery != queue acceptance",
receipt.target
),
),
};
let mut body = serde_json::json!({
"outcome": outcome,
"detail": detail,
"notify_id": id.to_string(),
"notify_state": receipt.state.as_str(),
"notify_target": receipt.target.to_string(),
"queued_at": receipt.queued_at.to_rfc3339(),
});
if let Some(at) = receipt.injected_at {
body["injected_at"] = serde_json::json!(at.to_rfc3339());
}
JsonRpcResponse::success(req_id, body)
}
}
}