2026-04-08 23:39:48 -04:00
|
|
|
// unconscious.rs — Graph maintenance agents
|
|
|
|
|
//
|
|
|
|
|
// Standalone agents that operate on the memory graph without needing
|
2026-04-09 00:41:18 -04:00
|
|
|
// conversation context. Each agent runs in a loop: finish one run,
|
2026-04-10 02:39:55 -04:00
|
|
|
// start the next. Agents can be toggled on/off, persisted to
|
|
|
|
|
// ~/.consciousness/agent-enabled.json.
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-10 02:39:55 -04:00
|
|
|
use std::time::Instant;
|
2026-04-09 00:51:10 -04:00
|
|
|
use std::collections::HashMap;
|
2026-04-09 01:00:48 -04:00
|
|
|
use futures::FutureExt;
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-11 21:34:41 -04:00
|
|
|
use crate::agent::oneshot::{AutoAgent, AutoStep, RunStats};
|
2026-04-08 23:39:48 -04:00
|
|
|
use crate::agent::tools;
|
|
|
|
|
use crate::subconscious::defs;
|
|
|
|
|
|
2026-04-09 00:51:10 -04:00
|
|
|
fn config_path() -> std::path::PathBuf {
|
|
|
|
|
dirs::home_dir().unwrap_or_default()
|
|
|
|
|
.join(".consciousness/agent-enabled.json")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn load_enabled_config() -> HashMap<String, bool> {
|
|
|
|
|
std::fs::read_to_string(config_path()).ok()
|
|
|
|
|
.and_then(|s| serde_json::from_str(&s).ok())
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn save_enabled_config(map: &HashMap<String, bool>) {
|
|
|
|
|
if let Ok(json) = serde_json::to_string_pretty(map) {
|
|
|
|
|
let _ = std::fs::write(config_path(), json);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 23:39:48 -04:00
|
|
|
struct UnconsciousAgent {
|
|
|
|
|
name: String,
|
2026-04-09 00:41:18 -04:00
|
|
|
enabled: bool,
|
2026-04-12 20:11:40 -04:00
|
|
|
auto: Option<AutoAgent>,
|
2026-04-12 02:04:50 -04:00
|
|
|
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<(), String>)>>,
|
2026-04-09 01:00:48 -04:00
|
|
|
/// Shared agent handle — UI locks to read context live.
|
|
|
|
|
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
|
2026-04-08 23:39:48 -04:00
|
|
|
last_run: Option<Instant>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl UnconsciousAgent {
|
|
|
|
|
fn is_running(&self) -> bool {
|
|
|
|
|
self.handle.as_ref().is_some_and(|h| !h.is_finished())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn should_run(&self) -> bool {
|
2026-04-10 02:39:55 -04:00
|
|
|
self.enabled && !self.is_running()
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Snapshot for the TUI.
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct UnconsciousSnapshot {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub running: bool,
|
2026-04-09 00:41:18 -04:00
|
|
|
pub enabled: bool,
|
|
|
|
|
pub runs: usize,
|
2026-04-08 23:39:48 -04:00
|
|
|
pub last_run_secs_ago: Option<f64>,
|
2026-04-09 01:00:48 -04:00
|
|
|
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
|
2026-04-10 13:44:41 -04:00
|
|
|
pub last_stats: Option<RunStats>,
|
2026-04-11 21:57:24 -04:00
|
|
|
/// Recent store activity for this agent: (key, timestamp), newest first.
|
|
|
|
|
pub history: Vec<(String, i64)>,
|
2026-04-11 22:12:46 -04:00
|
|
|
pub tool_calls_ewma: f64,
|
|
|
|
|
pub tool_failures_ewma: f64,
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Unconscious {
|
|
|
|
|
agents: Vec<UnconsciousAgent>,
|
|
|
|
|
max_concurrent: usize,
|
2026-04-09 00:45:26 -04:00
|
|
|
pub graph_health: Option<crate::subconscious::daemon::GraphHealth>,
|
|
|
|
|
last_health_check: Option<Instant>,
|
2026-04-13 22:38:01 -04:00
|
|
|
/// Notified when agent state changes (finished, toggled)
|
|
|
|
|
pub wake: std::sync::Arc<tokio::sync::Notify>,
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Unconscious {
|
|
|
|
|
pub fn new() -> Self {
|
2026-04-09 00:51:10 -04:00
|
|
|
let enabled_map = load_enabled_config();
|
|
|
|
|
|
|
|
|
|
// Scan all .agent files, exclude subconscious-* and surface-observe
|
|
|
|
|
let mut agents: Vec<UnconsciousAgent> = Vec::new();
|
2026-04-11 19:54:18 -04:00
|
|
|
let base_tools = tools::memory::memory_tools().to_vec();
|
|
|
|
|
let extra_tools = tools::memory::journal_tools().to_vec();
|
2026-04-09 00:51:10 -04:00
|
|
|
for def in defs::load_defs() {
|
|
|
|
|
if def.agent.starts_with("subconscious-") { continue; }
|
|
|
|
|
if def.agent == "surface-observe" { continue; }
|
|
|
|
|
let enabled = enabled_map.get(&def.agent).copied()
|
2026-04-09 01:00:48 -04:00
|
|
|
.unwrap_or(false);
|
2026-04-11 19:54:18 -04:00
|
|
|
let mut effective_tools = base_tools.clone();
|
|
|
|
|
for name in &def.tools {
|
|
|
|
|
if let Some(t) = extra_tools.iter().find(|t| t.name == name) {
|
|
|
|
|
effective_tools.push(t.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-09 01:00:48 -04:00
|
|
|
let steps: Vec<AutoStep> = def.steps.iter().map(|s| AutoStep {
|
|
|
|
|
prompt: s.prompt.clone(),
|
|
|
|
|
phase: s.phase.clone(),
|
|
|
|
|
}).collect();
|
|
|
|
|
let auto = AutoAgent::new(
|
|
|
|
|
def.agent.clone(), effective_tools, steps,
|
|
|
|
|
def.temperature.unwrap_or(0.6), def.priority,
|
|
|
|
|
);
|
2026-04-09 00:51:10 -04:00
|
|
|
agents.push(UnconsciousAgent {
|
|
|
|
|
name: def.agent.clone(),
|
|
|
|
|
enabled,
|
2026-04-12 20:11:40 -04:00
|
|
|
auto: Some(auto),
|
2026-04-09 00:51:10 -04:00
|
|
|
handle: None,
|
2026-04-09 01:00:48 -04:00
|
|
|
agent: None,
|
2026-04-09 00:51:10 -04:00
|
|
|
last_run: None,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
agents.sort_by(|a, b| a.name.cmp(&b.name));
|
|
|
|
|
|
2026-04-12 02:55:39 -04:00
|
|
|
let max_concurrent = crate::config::get().llm_concurrency;
|
|
|
|
|
|
2026-04-09 01:05:08 -04:00
|
|
|
Self {
|
2026-04-12 02:55:39 -04:00
|
|
|
agents, max_concurrent,
|
2026-04-09 00:45:26 -04:00
|
|
|
graph_health: None,
|
|
|
|
|
last_health_check: None,
|
2026-04-13 22:38:01 -04:00
|
|
|
wake: std::sync::Arc::new(tokio::sync::Notify::new()),
|
2026-04-09 01:05:08 -04:00
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 00:41:18 -04:00
|
|
|
/// Toggle an agent on/off by name. Returns new enabled state.
|
2026-04-09 00:53:54 -04:00
|
|
|
/// If enabling, immediately spawns the agent if it's not running.
|
2026-04-09 01:00:48 -04:00
|
|
|
pub async fn toggle(&mut self, name: &str) -> Option<bool> {
|
2026-04-09 00:53:54 -04:00
|
|
|
let idx = self.agents.iter().position(|a| a.name == name)?;
|
|
|
|
|
self.agents[idx].enabled = !self.agents[idx].enabled;
|
|
|
|
|
let new_state = self.agents[idx].enabled;
|
2026-04-09 00:51:10 -04:00
|
|
|
self.save_enabled();
|
2026-04-12 20:33:23 -04:00
|
|
|
if new_state && !self.agents[idx].is_running() && self.agents[idx].auto.is_some() {
|
|
|
|
|
let agent_name = self.agents[idx].name.clone();
|
|
|
|
|
let auto = self.agents[idx].auto.take().unwrap();
|
2026-04-13 22:38:01 -04:00
|
|
|
let wake = self.wake.clone();
|
|
|
|
|
match prepare_spawn(&agent_name, auto, wake).await {
|
2026-04-12 20:33:23 -04:00
|
|
|
Ok(result) => self.complete_spawn(idx, result),
|
|
|
|
|
Err(auto) => self.abort_spawn(idx, auto),
|
|
|
|
|
}
|
2026-04-09 00:53:54 -04:00
|
|
|
}
|
2026-04-13 22:38:01 -04:00
|
|
|
self.wake.notify_one(); // wake loop to consider new state
|
2026-04-09 00:51:10 -04:00
|
|
|
Some(new_state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn save_enabled(&self) {
|
|
|
|
|
let map: HashMap<String, bool> = self.agents.iter()
|
|
|
|
|
.map(|a| (a.name.clone(), a.enabled))
|
|
|
|
|
.collect();
|
|
|
|
|
save_enabled_config(&map);
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:57:24 -04:00
|
|
|
pub fn snapshots(&self, store: Option<&crate::store::Store>) -> Vec<UnconsciousSnapshot> {
|
|
|
|
|
self.agents.iter().map(|a| {
|
|
|
|
|
let history = store.map(|st| st.recent_by_provenance(&a.name, 30))
|
|
|
|
|
.unwrap_or_default();
|
2026-04-11 23:03:10 -04:00
|
|
|
let stats = crate::agent::oneshot::get_stats(&a.name);
|
|
|
|
|
let tool_calls_ewma: f64 = stats.by_tool.values().map(|t| t.ewma).sum();
|
2026-04-11 21:57:24 -04:00
|
|
|
UnconsciousSnapshot {
|
|
|
|
|
name: a.name.clone(),
|
|
|
|
|
running: a.is_running(),
|
|
|
|
|
enabled: a.enabled,
|
2026-04-11 23:03:10 -04:00
|
|
|
runs: stats.runs,
|
2026-04-11 21:57:24 -04:00
|
|
|
last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()),
|
|
|
|
|
agent: a.agent.clone(),
|
2026-04-11 23:03:10 -04:00
|
|
|
last_stats: stats.last_stats.clone(),
|
2026-04-11 21:57:24 -04:00
|
|
|
history,
|
2026-04-11 23:03:10 -04:00
|
|
|
tool_calls_ewma,
|
|
|
|
|
tool_failures_ewma: stats.failures.ewma,
|
2026-04-11 21:57:24 -04:00
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
}).collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 20:37:54 -04:00
|
|
|
/// Check if health refresh is due (quick check, no I/O).
|
|
|
|
|
pub fn needs_health_refresh(&self) -> bool {
|
|
|
|
|
self.last_health_check
|
|
|
|
|
.map(|t| t.elapsed() > std::time::Duration::from_secs(600))
|
|
|
|
|
.unwrap_or(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Store computed health (quick, just assignment).
|
|
|
|
|
pub fn set_health(&mut self, health: crate::subconscious::daemon::GraphHealth) {
|
|
|
|
|
self.graph_health = Some(health);
|
2026-04-09 00:45:26 -04:00
|
|
|
self.last_health_check = Some(Instant::now());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
/// Reap finished agents (quick, hold lock briefly).
|
|
|
|
|
pub fn reap_finished(&mut self) {
|
2026-04-08 23:39:48 -04:00
|
|
|
for agent in &mut self.agents {
|
|
|
|
|
if agent.handle.as_ref().is_some_and(|h| h.is_finished()) {
|
2026-04-09 01:00:48 -04:00
|
|
|
let handle = agent.handle.take().unwrap();
|
2026-04-08 23:39:48 -04:00
|
|
|
agent.last_run = Some(Instant::now());
|
2026-04-11 22:12:46 -04:00
|
|
|
// Get the AutoAgent back from the finished task (stats already updated)
|
2026-04-09 01:00:48 -04:00
|
|
|
match handle.now_or_never() {
|
2026-04-11 22:12:46 -04:00
|
|
|
Some(Ok((auto_back, result))) => {
|
2026-04-12 20:11:40 -04:00
|
|
|
agent.auto = Some(auto_back);
|
2026-04-09 01:00:48 -04:00
|
|
|
match result {
|
|
|
|
|
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
|
2026-04-11 23:03:10 -04:00
|
|
|
agent.name, crate::agent::oneshot::get_stats(&agent.name).runs),
|
2026-04-09 01:00:48 -04:00
|
|
|
Err(e) => dbglog!("[unconscious] {} failed: {}", agent.name, e),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => dbglog!("[unconscious] {} task lost", agent.name),
|
|
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-12 20:33:23 -04:00
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
/// Select agents to spawn and take their AutoAgents out (quick, hold lock briefly).
|
|
|
|
|
/// Returns vec of (index, name, auto, tools) for agents that should spawn.
|
|
|
|
|
pub fn select_to_spawn(&mut self) -> Vec<(usize, String, AutoAgent)> {
|
2026-04-08 23:39:48 -04:00
|
|
|
let running = self.agents.iter().filter(|a| a.is_running()).count();
|
2026-04-12 20:33:23 -04:00
|
|
|
let mut to_spawn = Vec::new();
|
|
|
|
|
|
2026-04-10 03:20:20 -04:00
|
|
|
for _ in running..self.max_concurrent {
|
|
|
|
|
let next = self.agents.iter().enumerate()
|
2026-04-12 20:33:23 -04:00
|
|
|
.filter(|(_, a)| a.should_run() && a.auto.is_some())
|
2026-04-10 03:20:20 -04:00
|
|
|
.min_by_key(|(_, a)| a.last_run);
|
|
|
|
|
match next {
|
2026-04-12 20:33:23 -04:00
|
|
|
Some((idx, _)) => {
|
|
|
|
|
let name = self.agents[idx].name.clone();
|
|
|
|
|
let auto = self.agents[idx].auto.take().unwrap();
|
|
|
|
|
to_spawn.push((idx, name, auto));
|
|
|
|
|
}
|
2026-04-10 03:20:20 -04:00
|
|
|
None => break,
|
|
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
2026-04-12 20:33:23 -04:00
|
|
|
to_spawn
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
/// Store spawn result back (quick, hold lock briefly).
|
|
|
|
|
pub fn complete_spawn(&mut self, idx: usize, result: SpawnResult) {
|
|
|
|
|
self.agents[idx].agent = Some(result.agent);
|
|
|
|
|
self.agents[idx].handle = Some(result.handle);
|
|
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
/// Restore auto on spawn failure (quick, hold lock briefly).
|
|
|
|
|
pub fn abort_spawn(&mut self, idx: usize, auto: AutoAgent) {
|
|
|
|
|
self.agents[idx].auto = Some(auto);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
/// Result of preparing an agent spawn (created outside the lock).
|
|
|
|
|
pub struct SpawnResult {
|
|
|
|
|
pub agent: std::sync::Arc<crate::agent::Agent>,
|
|
|
|
|
pub handle: tokio::task::JoinHandle<(AutoAgent, Result<(), String>)>,
|
|
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
/// Prepare an agent spawn — does the slow work (Store::load, query, Agent::new).
|
|
|
|
|
/// Called outside the Unconscious lock.
|
|
|
|
|
/// On success, auto is consumed (moved into spawned task).
|
|
|
|
|
/// On failure, auto is returned so it can be restored.
|
2026-04-13 22:38:01 -04:00
|
|
|
pub async fn prepare_spawn(name: &str, mut auto: AutoAgent, wake: std::sync::Arc<tokio::sync::Notify>) -> Result<SpawnResult, AutoAgent> {
|
2026-04-12 20:33:23 -04:00
|
|
|
dbglog!("[unconscious] spawning {}", name);
|
|
|
|
|
|
|
|
|
|
let def = match defs::get_def(name) {
|
|
|
|
|
Some(d) => d,
|
|
|
|
|
None => return Err(auto),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let exclude: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
|
|
|
let batch = match defs::run_agent(
|
2026-04-13 15:18:05 -04:00
|
|
|
&def, def.count.unwrap_or(5), &exclude,
|
2026-04-13 14:55:41 -04:00
|
|
|
).await {
|
2026-04-12 20:33:23 -04:00
|
|
|
Ok(b) => b,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
dbglog!("[unconscious] {} query failed: {}", name, e);
|
|
|
|
|
return Err(auto);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
let orig_steps = std::mem::replace(&mut auto.steps,
|
|
|
|
|
batch.steps.iter().map(|s| AutoStep {
|
|
|
|
|
prompt: s.prompt.clone(),
|
|
|
|
|
phase: s.phase.clone(),
|
|
|
|
|
}).collect());
|
|
|
|
|
|
|
|
|
|
// Create standalone Agent — stored so UI can read context
|
|
|
|
|
let config = crate::config::get();
|
|
|
|
|
let base_url = config.api_base_url.as_deref().unwrap_or("");
|
|
|
|
|
let api_key = config.api_key.as_deref().unwrap_or("");
|
|
|
|
|
let model = config.api_model.as_deref().unwrap_or("");
|
|
|
|
|
if base_url.is_empty() || model.is_empty() {
|
|
|
|
|
dbglog!("[unconscious] API not configured");
|
|
|
|
|
auto.steps = orig_steps;
|
|
|
|
|
return Err(auto);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cli = crate::user::CliArgs::default();
|
|
|
|
|
let (app, _) = match crate::config::load_app(&cli) {
|
|
|
|
|
Ok(r) => r,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
dbglog!("[unconscious] config: {}", e);
|
2026-04-09 01:00:48 -04:00
|
|
|
auto.steps = orig_steps;
|
2026-04-12 20:33:23 -04:00
|
|
|
return Err(auto);
|
2026-04-09 01:00:48 -04:00
|
|
|
}
|
2026-04-12 20:33:23 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Unconscious agents have self-contained prompts — no standard context.
|
|
|
|
|
let client = crate::agent::api::ApiClient::new(base_url, api_key, model);
|
|
|
|
|
let agent = crate::agent::Agent::new(
|
|
|
|
|
client, Vec::new(),
|
|
|
|
|
app, String::new(), None,
|
|
|
|
|
crate::agent::tools::ActiveTools::new(),
|
|
|
|
|
auto.tools.clone(),
|
|
|
|
|
).await;
|
|
|
|
|
{
|
|
|
|
|
let mut st = agent.state.lock().await;
|
|
|
|
|
st.provenance = auto.name.clone();
|
|
|
|
|
st.priority = Some(auto.priority);
|
|
|
|
|
st.temperature = auto.temperature;
|
|
|
|
|
}
|
2026-04-09 01:00:48 -04:00
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
let agent_clone = agent.clone();
|
|
|
|
|
let handle = tokio::spawn(async move {
|
|
|
|
|
let result = auto.run_shared(&agent_clone).await;
|
|
|
|
|
let stats = crate::agent::oneshot::save_agent_log(&auto.name, &agent_clone).await;
|
|
|
|
|
auto.update_stats(stats);
|
|
|
|
|
auto.steps = orig_steps;
|
2026-04-13 22:38:01 -04:00
|
|
|
wake.notify_one(); // wake the loop to reap and maybe spawn more
|
2026-04-12 20:33:23 -04:00
|
|
|
(auto, result)
|
|
|
|
|
});
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
Ok(SpawnResult { agent, handle })
|
|
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
|
2026-04-12 20:33:23 -04:00
|
|
|
// Backwards compat: trigger() that does all three phases (still holds lock too long, but works)
|
|
|
|
|
impl Unconscious {
|
|
|
|
|
pub async fn trigger(&mut self) {
|
|
|
|
|
self.reap_finished();
|
|
|
|
|
let to_spawn = self.select_to_spawn();
|
2026-04-13 22:38:01 -04:00
|
|
|
let wake = self.wake.clone();
|
2026-04-12 20:33:23 -04:00
|
|
|
for (idx, name, auto) in to_spawn {
|
2026-04-13 22:38:01 -04:00
|
|
|
match prepare_spawn(&name, auto, wake.clone()).await {
|
2026-04-12 20:33:23 -04:00
|
|
|
Ok(result) => self.complete_spawn(idx, result),
|
|
|
|
|
Err(auto) => self.abort_spawn(idx, auto),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 02:39:55 -04:00
|
|
|
|
2026-04-11 21:34:41 -04:00
|
|
|
// save_agent_log and RunStats moved to crate::agent::oneshot
|