mod auth_command;
mod brand;
mod chat;
mod codex_env;
mod color;
mod context_window;
mod crew_form;
mod help;
#[cfg(feature = "rich-tui")]
mod inference_panel;
mod migration_notices;
mod navigator_cmds;
mod session_capability;
mod workspace_launch;
mod workspace_settings;
pub use workspace_launch::{
resolve_code_workspace, workspace_protection_for_diagnostics, CodeWorkspace,
};
pub(crate) use session_capability::SessionCapability;
pub use auth_command::run_auth;
use codex_env::{codex_env_allowed, codex_env_backend};
mod turn_input;
pub(crate) use turn_input::{with_interrupt_watch, with_live_spill_watch};
mod roadmap_cmds;
pub(crate) use roadmap_cmds::{
autocapture_commit_after_turn, git_head_short, handle_roadmap_command, roadmap_subcommand_reads,
};
mod persona_route;
mod preference_pins;
pub(crate) use preference_pins::{
apply_startup_preference_pin, restore_preference_pin, ConversationPreferenceSwitch,
PinDegraded, PinRestore, PreferenceBaseline, StartupConversation,
};
pub(crate) use persona_route::apply_persona_backend;
mod commands;
#[cfg(feature = "live-spill")]
mod completed_spill;
mod danger;
pub mod dgx_probe;
mod event_receipt;
pub mod herdr;
#[cfg(feature = "rich-tui")]
mod inline_viewport;
#[cfg(feature = "rich-tui")]
mod interaction_view;
#[cfg(feature = "rich-tui")]
mod clarification_modal;
#[cfg(all(test, unix, feature = "rich-tui"))]
mod interaction_view_pty_test;
#[cfg(feature = "live-spill")]
mod live_spill;
#[cfg(all(test, unix, feature = "rich-tui"))]
mod panel_raw_mode_pty_test;
mod permissions;
mod prompt;
#[cfg(all(test, unix))]
mod prompt_visibility_test;
#[cfg(all(test, unix))]
mod interrupt_ack_pty_test;
#[cfg(any(feature = "rich-tui", feature = "live-spill"))]
mod scrollbar;
mod settings_form;
#[cfg(all(test, unix, feature = "rich-tui"))]
mod settings_form_pty_test;
#[cfg(feature = "rich-tui")]
mod shell;
mod slash_registry;
#[cfg(feature = "live-spill")]
mod spill_view;
mod status_topics;
mod tab_switch;
mod tabs;
#[cfg(all(test, unix, feature = "rich-tui"))]
mod transcript_pager_pty_test;
#[cfg_attr(not(feature = "rich-tui"), allow(dead_code))]
mod transcript_pager;
mod type_ahead;
mod mcp;
#[cfg(feature = "rich-tui")]
mod mcp_manager;
#[cfg(feature = "rich-tui")]
mod mcp_panel;
mod mcp_token;
pub mod probe;
pub mod terminal_hyperlink;
mod window_discovery;
mod workspace_state;
#[cfg(feature = "rich-tui")]
mod rich_input;
#[cfg(all(test, unix, feature = "rich-tui"))]
mod rich_input_pty_test;
#[cfg(feature = "rich-tui")]
mod config_panel;
#[cfg(feature = "rich-tui")]
mod lines_panel;
#[cfg(feature = "rich-tui")]
mod palette;
#[cfg(feature = "rich-tui")]
mod theme;
#[cfg(feature = "rich-tui")]
mod modal_size;
#[cfg(feature = "rich-tui")]
mod panel;
#[cfg(feature = "rich-tui")]
mod panel_controls;
#[cfg(feature = "rich-tui")]
mod prefix;
#[cfg(feature = "rich-tui")]
mod permissions_panel;
#[cfg(feature = "rich-tui")]
mod settings_panel;
#[cfg(feature = "rich-tui")]
mod theme_panel;
#[cfg(feature = "rich-tui")]
mod backend_panel;
#[cfg(feature = "rich-tui")]
mod backend_chooser;
#[cfg(feature = "rich-tui")]
mod vi;
#[cfg(all(unix, feature = "rich-tui"))]
mod esc_ladder;
#[cfg(all(test, unix, feature = "rich-tui"))]
mod esc_ladder_pty_test;
#[cfg(feature = "rich-tui")]
mod cockpit;
#[cfg(all(unix, any(feature = "rich-tui", feature = "live-spill")))]
mod mouse;
#[cfg(all(unix, any(feature = "rich-tui", feature = "live-spill")))]
pub use mouse::install_panic_release_hook;
#[cfg(feature = "rich-tui")]
mod modal;
mod lean_input;
#[cfg(feature = "rich-tui")]
mod list_cursor;
#[cfg(feature = "rich-tui")]
mod models_panel;
mod session_worker;
mod setup;
mod setup_tui;
mod tab_bar;
mod wizard;
use anyhow::Context as _;
use mcp::Mcp;
pub(crate) use brand::{
brand_active, brand_logo, brand_name, brand_plugins, brand_tagline, logo_for_size, LOGO_20,
LOGO_PLAIN, NEWT_ORANGE, VERSION,
};
#[cfg(feature = "rich-tui")]
pub(crate) use chat::footer_continues;
use chat::run_chat;
pub(crate) use chat::{InputSurface, ReadOutcome};
pub use color::color_supported;
use color::{color_enabled_for, resolve_color_mode};
#[cfg(any(feature = "rich-tui", test))]
pub(crate) use help::help_lines;
pub use help::render_help;
#[cfg(test)]
use help::{canonical_help_topic, command_help_page};
use help::{print_command_help, render_help_for_tui};
#[cfg(test)]
use newt_core::agentic::newt_line;
use newt_core::agentic::{print_harness_notice, print_newt, ChatCtx, NEWT_ORANGE_CT};
use newt_core::recover_context_window_400;
#[cfg(test)]
use prompt::expand_prompt_tokens;
#[cfg(feature = "rich-tui")]
use prompt::resolve_gutter_setting;
#[cfg(feature = "rich-tui")]
use prompt::rich_surface_selected;
use prompt::{
current_prompt_and_preview, debug_mode, footer_mode, footer_rich_enabled, prompt_str,
prompt_token_help, resolve_edit_mode, strip_one_quote_pair, trace_mode, verbose_mode,
};
pub use prompt::{DEFAULT_LEAN_PROMPT, DEFAULT_RICH_PROMPT, PROMPT_TOKENS};
use workspace_state::workspace_state_block;
#[cfg(test)]
use workspace_state::{
format_workspace_state_block, parse_git_porcelain_dirty_files, WorkspaceStateSnapshot,
};
mod splash;
pub fn run_init(color: bool) -> anyhow::Result<()> {
wizard::run_init(color)
}
pub fn run_setup(color: bool) -> anyhow::Result<()> {
setup::run(color)
}
pub async fn run_setup_target_with_model(
target: &str,
token_env: Option<&str>,
token_file: Option<&std::path::Path>,
model: Option<&str>,
yes: bool,
config_path: Option<&std::path::Path>,
) -> anyhow::Result<()> {
setup::run_target(target, token_env, token_file, model, yes, config_path).await
}
pub fn run_crew_edit(name: Option<&str>, color: bool) -> anyhow::Result<()> {
crew_form::run_edit(name, color)
}
pub(crate) fn run_crew_edit_with_ask(
name: Option<&str>,
color: bool,
ask: SlashAsk<'_>,
) -> anyhow::Result<()> {
crew_form::run_edit_with_ask(name, color, ask)
}
#[cfg(feature = "rich-tui")]
pub(crate) fn run_psyche_panel(
seed: config_panel::PanelSeed,
persist: impl FnMut(&str, &str, bool) -> config_panel::SaveResult,
window: Option<session_worker::PanelWindow>,
color: bool,
verbose: bool,
) -> config_panel::PanelOutcome {
if std::io::IsTerminal::is_terminal(&std::io::stdout()) {
match config_panel::run(seed, persist, window) {
Ok(outcome) => outcome,
Err(e) => {
print_newt(&format!("psyche panel error: {e}"), color, verbose);
config_panel::PanelOutcome::Cancelled
}
}
} else {
print_newt(
"the psyche panel needs an interactive rich terminal — use /psyche status \
for the text view, or /psyche cognition / tenacity / initiative <level> \
to change the dials.",
color,
verbose,
);
config_panel::PanelOutcome::Cancelled
}
}
use std::io::{self, IsTerminal, Write as _};
use std::path::{Path, PathBuf};
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute,
style::{Color as CtColor, Print, ResetColor, SetForegroundColor},
terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use newt_core::tty::raw_mode::RawModeGuard;
#[cfg(all(test, feature = "rich-tui"))]
pub(crate) fn production_source(src: &str) -> &str {
["\n#[cfg(test)]\nmod tests {", "\n#[cfg(test)]\n#[path = "]
.iter()
.filter_map(|marker| src.split(marker).next())
.filter(|prefix| prefix.len() < src.len())
.min_by_key(|prefix| prefix.len())
.expect(
"the file must end in an unindented `#[cfg(test)] mod tests {` or \
`#[cfg(test)] #[path = …]` — without one of those markers this \
helper would hand back an empty string, and every count-based \
assertion would pass having read nothing",
)
}
struct RestoreOnDrop<F: FnMut()> {
restore: F,
}
impl<F: FnMut()> Drop for RestoreOnDrop<F> {
fn drop(&mut self) {
(self.restore)();
}
}
struct SplashScreenGuard {
_restore: RestoreOnDrop<fn()>,
_region: newt_core::tty::RegionLease,
_raw: RawModeGuard,
}
impl SplashScreenGuard {
fn enter() -> io::Result<Self> {
let raw = RawModeGuard::enter()?;
let region = newt_core::tty::Terminal::lease_region(
newt_core::tty::Region::WholeScreen,
newt_core::tty::OnCollision::SuspendHolder,
)
.ok_or_else(|| io::Error::other("the screen could not be leased"))?;
let guard = Self {
_restore: RestoreOnDrop {
restore: || {
let _ = execute!(io::stdout(), Show, LeaveAlternateScreen);
},
},
_region: region,
_raw: raw,
};
#[cfg(unix)]
unsafe {
libc::tcflush(libc::STDIN_FILENO, libc::TCIFLUSH);
}
execute!(
io::stdout(),
EnterAlternateScreen,
Hide,
Clear(ClearType::All),
MoveTo(0, 0)
)?;
Ok(guard)
}
}
#[cfg(test)]
#[path = "lib_tests/splash_guard_tests.rs"]
mod splash_guard_tests;
#[cfg(test)]
#[path = "lib_tests/slash_ask_seam_tests.rs"]
mod slash_ask_seam_tests;
pub fn run_code(
path: Option<&std::path::Path>,
no_splash: bool,
persona: Option<&str>,
altitude: Option<newt_core::Altitude>,
crew_runner: Option<&dyn newt_core::agentic::CrewRunner>,
setup: Option<SetupHandle>,
) -> anyhow::Result<()> {
let mut migration_notices = crate::migration_notices::Pending::default();
let cfg_color = newt_core::Config::resolve(&mut |notice| migration_notices.report(notice))
.ok()
.and_then(|c| c.tui)
.map(|t| t.color)
.unwrap_or_default();
let color = color_enabled_for(
resolve_color_mode(&|k| std::env::var(k).ok(), cfg_color),
io::stdout().is_terminal(),
);
if let Some(p) = path {
newt_core::settings::set_workspace(p);
}
let workspace = resolve_workspace(path);
let inline = no_splash;
if !inline {
let screen = SplashScreenGuard::enter()?;
let mut stdout = io::stdout();
let prewarm = spawn_backend_prewarm(&mut |notice| migration_notices.report(notice));
let unconfigured =
newt_core::Config::resolve(&mut |notice| migration_notices.report(notice))
.map(|c| c.is_unconfigured())
.unwrap_or(true);
let context = if unconfigured || setup.is_some() {
"initial setup"
} else {
"starting"
};
let status = if prewarm.is_some() {
"warming up backend…"
} else {
"initializing…"
};
let cont = splash::show_splash(&mut stdout, &workspace, color, status, context, setup)?;
drop(screen);
migration_notices.flush();
if !cont {
if let Some(pw) = prewarm {
pw.handle.abort();
}
return Ok(());
}
if unconfigured {
print!("{}", brand::crawl_header(Some("initial setup")));
}
wizard::maybe_run(color)?;
print_inline_header(&workspace, color);
return run_chat(&workspace, color, persona, altitude, crew_runner, prewarm);
}
migration_notices.flush();
wizard::maybe_run(color)?;
if let Some(setup) = setup {
print_inline_header(&workspace, color);
run_setup_inline(&setup);
return run_chat(&workspace, color, persona, altitude, crew_runner, None);
}
print_inline_header(&workspace, color);
run_chat(&workspace, color, persona, altitude, crew_runner, None)
}
pub(crate) struct Prewarm {
pub(crate) url: String,
pub(crate) handle:
tokio::task::JoinHandle<Option<newt_core::backend_probe::EndpointProbeResult>>,
}
pub(crate) fn prewarm_applies(choice_url: &str, prewarm_url: &str) -> bool {
choice_url.trim_end_matches('/') == prewarm_url.trim_end_matches('/')
}
pub(crate) fn probe_timeout_secs(url: &str) -> u64 {
if url.starts_with("https://") {
10
} else {
1
}
}
fn spawn_backend_prewarm(
report: &mut dyn FnMut(newt_core::tty::Notice<'static>),
) -> Option<Prewarm> {
let runtime = tokio::runtime::Handle::try_current().ok()?;
let cfg = newt_core::Config::resolve_runtime_unpublished(report).ok()?;
if cfg.is_unconfigured() {
return None;
}
let choice = resolve_backend_choice(&cfg).ok()?;
let url = choice.url.clone();
let api_key = choice.api_key.clone();
let needs_probe = choice.kind_needs_probe;
let kind = choice.kind;
let secs = probe_timeout_secs(&url);
let task_url = url.clone();
let handle = runtime.spawn(async move {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(secs))
.build()
.ok()?;
if needs_probe {
newt_core::backend_probe::detect_endpoint(&client, &task_url, api_key.as_deref())
.await
.ok()
} else {
let api = newt_core::backend_probe::api_for(kind);
let models = api
.list_models(&client, &task_url, api_key.as_deref())
.await
.ok()?;
let warm = api
.warm_models(&client, &task_url, api_key.as_deref())
.await
.unwrap_or_default();
Some(newt_core::backend_probe::EndpointProbeResult {
endpoint: task_url.trim_end_matches('/').to_string(),
serving: api.serving(models.len()),
kind,
models,
warm,
engine: None,
})
}
});
Some(Prewarm { url, handle })
}
fn print_inline_header(workspace: &str, color: bool) {
print!("{}", render_inline_header(workspace, color));
}
fn render_inline_header(workspace: &str, color: bool) -> String {
use std::fmt::Write as _;
if !color {
let plugins = brand_plugins()
.map(|p| format!(" · {p}"))
.unwrap_or_default();
return format!("{} v{VERSION} · {workspace}{plugins}\n\n", brand_name());
}
let text_col = 23u16;
let logo = brand_logo(LOGO_20, "ansi-20");
let logo_lines: Vec<&str> = logo.lines().collect();
let n = logo_lines.len();
let mid = n / 2;
let header = brand_tagline();
let plugins = brand_plugins();
let version = format!("v{VERSION}");
let text: &[(&str, bool)] = &[
(header.as_str(), false),
(version.as_str(), true), (plugins.as_deref().unwrap_or(""), true), (
"ready — type a task, /help for commands, /exit to quit",
true,
),
("keybindings — /vi (default) · /emacs · /nano", true),
];
let text_start = mid.saturating_sub(1);
let mut out = String::new();
for (i, logo_line) in logo_lines.iter().enumerate() {
out.push_str(logo_line);
let ti = i.wrapping_sub(text_start);
if let Some((msg, dim)) = text.get(ti) {
if !msg.is_empty() {
let style_on = if *dim { "\x1b[38;2;100;100;100m" } else { "" };
let style_off = if *dim { "\x1b[0m" } else { "" };
let _ = write!(out, "\x1b[{text_col}G{style_on}{msg}{style_off}");
}
}
out.push('\n');
}
out.push('\n');
out
}
pub fn run_pilot(_flight_id: &str) -> anyhow::Result<()> {
anyhow::bail!("newt-tui::run_pilot not yet implemented")
}
pub use crate::permissions::ocap_high_danger_predicate;
#[cfg(unix)]
use crate::permissions::try_watch_stdin;
use crate::permissions::{
permission_prompting_configured, production_danger_table, prompt_permission_choice,
should_prompt_permissions, PermissionPromptState, PromptChoice, PromptPermissionGate,
};
use crate::setup_tui::run_setup_inline;
pub use crate::setup_tui::{SetupEvent, SetupHandle};
pub(crate) fn today_date() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let days = secs / 86400;
let z = days as i64 + 719468;
let era = z.div_euclid(146097);
let doe = z.rem_euclid(146097) as u32;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{y:04}-{m:02}-{d:02}")
}
#[cfg(unix)]
fn mark_fds_cloexec() {
let max = unsafe { libc::sysconf(libc::_SC_OPEN_MAX) };
let max_fd = if max > 0 {
max.min(4096) as libc::c_int
} else {
256
};
for fd in 3..max_fd {
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags >= 0 && (flags & libc::FD_CLOEXEC == 0) {
libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
}
}
}
}
fn terminal_fd_available() -> bool {
terminal_fd_available_from_probe(|| std::fs::File::open(null_device_path()).map(drop))
}
fn terminal_fd_available_from_probe(probe: impl FnOnce() -> std::io::Result<()>) -> bool {
probe().is_ok()
}
#[cfg(windows)]
fn null_device_path() -> &'static str {
"NUL"
}
#[cfg(not(windows))]
fn null_device_path() -> &'static str {
"/dev/null"
}
#[cfg(unix)]
fn scan_cli_exec_grants() -> Vec<String> {
use std::os::unix::fs::PermissionsExt;
let mut dirs: Vec<String> = Vec::new();
if let Ok(venv) = std::env::var("NEWT_VENV") {
dirs.push(format!("{venv}/bin"));
}
if let Ok(paths) = std::env::var("NEWT_EXEC_PATHS") {
for p in paths.split(':') {
if !p.is_empty() {
dirs.push(p.to_string());
}
}
}
dirs.iter()
.flat_map(|dir| std::fs::read_dir(dir).ok().into_iter().flatten())
.flatten()
.filter_map(|e| {
let path = e.path();
if !path.is_file() {
return None;
}
let mode = path.metadata().ok()?.permissions().mode();
if mode & 0o111 == 0 {
return None;
}
std::path::absolute(path).ok()?.to_str().map(str::to_string)
})
.collect()
}
#[cfg(not(unix))]
fn scan_cli_exec_grants() -> Vec<String> {
Vec::new()
}
fn runtime_authority_note(disposition: newt_core::agentic::PromptDisposition) -> Option<String> {
let switches = (
newt_core::agentic::ocap_disabled(),
newt_core::agentic::full_access_requested(),
);
let active = match switches {
(true, true) => "--disable-ocap/--yolo AND --full-access are",
(true, false) => "--disable-ocap/--yolo is",
(false, true) => "--full-access is",
(false, false) => return None,
};
if !newt_core::agentic::tool_allowed(disposition, "run_command") {
return Some(format!(
"Runtime authority: {active} active for this session, but this request is \
limited to the tools in your catalog. Use those or answer directly; do not \
call a tool that is not listed."
));
}
let note = match switches {
(true, true) =>
"Runtime authority: --disable-ocap/--yolo AND --full-access are active. \
run_command is available with unrestricted exec authority and uses the \
unconfined host shell; the configured [tui.permissions] preset and its \
exec floor are lifted. A posture, persona, or operating mode may still \
attenuate that session baseline. \
Do not claim a capability wall without first calling run_command and \
grounding the claim in its returned tool result. Native fs tools are \
unrestricted; web_fetch has unrestricted configured net authority.",
(true, false) =>
"Runtime authority: --disable-ocap/--yolo is active. run_command uses the \
unconfined host shell when the active exec floor permits it, not the \
brush/agent-bridle confined shell. Do not claim run_command is unavailable \
due to brush in this mode. Native fs tools remain workspace-fenced; \
web_fetch remains net-leashed.",
_ =>
"Runtime authority: --full-access is active. run_command is available with \
unrestricted exec authority; the configured permission preset and exec \
floor are lifted for this invocation, though a posture, persona, or \
operating mode may still attenuate that session baseline. Do not claim a capability wall \
without first calling run_command and grounding the claim in its returned \
tool result. Native fs tools and configured net authority are unrestricted.",
};
Some(note.to_string())
}
fn filesystem_authority_note() -> &'static str {
if newt_core::agentic::full_access_requested() {
"# Filesystem authority\n\
The full-access session baseline permits reads and writes outside the \
workspace. A posture, persona, or operating mode may still attenuate it. \
Treat an actual fs_read/fs_write tool denial as authoritative; do not \
infer confinement without a returned tool result.\n"
} else {
"# Filesystem confinement\n\
You are confined to the workspace (the current directory) plus any paths \
the operator explicitly opened. A read or write outside that returns \
`capability denied: fs_read/fs_write does not permit '<path>'`. Do NOT \
retry a denied path or try to work around it — instead tell the operator \
it's outside your workspace and that they can relaunch with \
`--read <path>` (read-only) or `--write <path>` (read+write) to grant it.\n"
}
}
fn runtime_context_block(
model: &str,
endpoint: &str,
kind: newt_core::BackendKind,
identity: &newt_core::AgentIdentity,
disposition: newt_core::agentic::PromptDisposition,
) -> String {
let now = chrono::Local::now()
.format("%Y-%m-%d %H:%M:%S %Z")
.to_string();
let backend = match kind {
newt_core::BackendKind::Openai => "openai-compatible",
newt_core::BackendKind::Anthropic => "anthropic",
_ => "ollama",
};
let author_email = identity.email.as_str();
let author_name = identity.name.as_str();
let runtime_authority = runtime_authority_note(disposition)
.map(|note| format!("# Runtime authority\n{note}\n"))
.unwrap_or_default();
let filesystem_authority = filesystem_authority_note();
format!(
"# Environment (refreshed every turn)\n\
Harness: newt-agent v{VERSION}\n\
Model: {model}\n\
Backend: {backend} @ {endpoint}\n\
Current date/time: {now}\n\
You are the model named above, running under the newt-agent harness. \
When asked who or what you are — and when attributing work (commit \
trailers, git notes, PR text) — use this real model name and harness; \
never invent or guess an identity.\n\
{runtime_authority}\
# Git commit identity\n\
Configured Git identity: `{author_name} <{author_email}>`. \
Use ordinary Git commands for repository work. Harness-managed commits \
record every contributing model/harness pair from the attribution ledger. \
Never invent an author identity or claim a commit or signature without \
a successful tool result.\n\
{filesystem_authority}"
)
}
fn read_only_caveats(workspace: &str) -> newt_core::caveats::Caveats {
newt_core::ToolPermissions {
preset: newt_core::PermissionPreset::ReadOnly,
extra_exec: Vec::new(),
net: Vec::new(),
prompt: false,
..Default::default()
}
.to_caveats(workspace)
}
fn policy_for(tui: Option<newt_core::TuiConfig>, workspace: &str) -> newt_core::caveats::Caveats {
policy_for_launch(tui, workspace, newt_core::launch_authority::current())
}
fn policy_for_launch(
tui: Option<newt_core::TuiConfig>,
workspace: &str,
authority: newt_core::launch_authority::LaunchAuthority,
) -> newt_core::caveats::Caveats {
use newt_core::caveats::Scope;
let mut caveats = if authority.full_access() {
newt_core::caveats::Caveats::top()
} else if authority.workspace_access() {
let mut permissions = tui.map(|t| t.permissions).unwrap_or_default();
permissions.preset = newt_core::PermissionPreset::WorkspaceFullAccess;
permissions.to_caveats(workspace)
} else {
tui.map(|t| t.permissions.to_caveats(workspace))
.unwrap_or_else(|| read_only_caveats(workspace))
};
let extra = scan_cli_exec_grants();
if !extra.is_empty() {
if let Scope::Only(ref mut set) = caveats.exec {
set.extend(extra);
}
}
let reads_deliberately_unbounded =
matches!(caveats.fs_read, Scope::All) && matches!(caveats.fs_write, Scope::All);
if !reads_deliberately_unbounded {
newt_core::caveats::apply_cli_fs_grants(&mut caveats, workspace);
}
caveats
}
fn resolve_tui(cfg: &newt_core::Config) -> Option<newt_core::TuiConfig> {
cfg.tui.clone()
}
pub(crate) fn mint_operating_key(
key_path: &std::path::Path,
policy: &newt_core::caveats::Caveats,
) -> Result<newt_identity::AgentKey, newt_identity::IdentityError> {
let user = newt_identity::load_or_generate(key_path)?;
let root = newt_identity::session_root(&user);
newt_identity::attenuate(&root, policy)
}
pub(crate) use newt_core::operating_mode::OperatingMode;
#[derive(Debug, Default)]
struct AutoModeState {
selected: std::sync::Mutex<Option<(String, OperatingMode)>>,
}
impl AutoModeState {
fn take_for(&self, conversation_id: &str) -> Option<OperatingMode> {
let Ok(mut selected) = self.selected.lock() else {
return None;
};
match selected.take() {
Some((bound_id, mode)) if bound_id == conversation_id => Some(mode),
Some(_) | None => None,
}
}
#[cfg(test)]
fn pending_for(&self, conversation_id: &str) -> Option<OperatingMode> {
self.selected
.lock()
.ok()
.and_then(|selected| match selected.as_ref() {
Some((bound_id, mode)) if bound_id == conversation_id => Some(*mode),
_ => None,
})
}
fn clear(&self) {
if let Ok(mut selected) = self.selected.lock() {
*selected = None;
}
}
fn bind<'a>(&'a self, conversation_id: &'a str) -> TurnAutoModeControl<'a> {
TurnAutoModeControl {
state: self,
conversation_id,
}
}
}
#[derive(Debug, Default)]
struct PlanModeState {
active: std::sync::atomic::AtomicBool,
exit_requested: std::sync::atomic::AtomicBool,
implementing_approved: std::sync::atomic::AtomicBool,
}
impl PlanModeState {
fn is_active(&self) -> bool {
self.active.load(std::sync::atomic::Ordering::Acquire)
}
fn set_implementing_approved(&self, seeded_by_approval: bool) {
self.implementing_approved
.store(seeded_by_approval, std::sync::atomic::Ordering::Release);
}
fn clear(&self) {
self.active
.store(false, std::sync::atomic::Ordering::Release);
self.exit_requested
.store(false, std::sync::atomic::Ordering::Release);
self.implementing_approved
.store(false, std::sync::atomic::Ordering::Release);
}
}
impl newt_core::agentic::PlanModeControl for PlanModeState {
fn is_plan_mode(&self) -> bool {
self.is_active()
}
fn set_plan_mode(&self, active: bool) -> Result<(), String> {
self.active
.store(active, std::sync::atomic::Ordering::Release);
Ok(())
}
fn request_exit(&self) -> Result<(), String> {
self.exit_requested
.store(true, std::sync::atomic::Ordering::Release);
Ok(())
}
fn take_exit_requested(&self) -> bool {
self.exit_requested
.swap(false, std::sync::atomic::Ordering::AcqRel)
}
fn exit_requested(&self) -> bool {
self.exit_requested
.load(std::sync::atomic::Ordering::Acquire)
}
fn implementing_approved_plan(&self) -> bool {
self.implementing_approved
.load(std::sync::atomic::Ordering::Acquire)
}
}
#[derive(Debug, Default)]
struct PlanDraftState {
latest: std::sync::Mutex<Option<ObjectiveDraft>>,
presented: std::sync::Mutex<Option<ObjectivePresented>>,
}
#[derive(Debug, Clone)]
struct ObjectiveDraft {
objective: newt_core::PromptId,
draft: newt_core::agentic::PlanDraft,
}
#[derive(Debug, Clone)]
struct ObjectivePresented {
objective: newt_core::PromptId,
plan: newt_core::agentic::PresentedPlan,
}
type PlanDraftPersist<'a> = &'a (dyn Fn(&std::path::Path, &str) -> std::io::Result<()> + Sync);
fn persist_plan_to_disk(path: &std::path::Path, markdown: &str) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, markdown)
}
impl PlanDraftState {
fn clear(&self) {
if let Ok(mut latest) = self.latest.lock() {
*latest = None;
}
if let Ok(mut presented) = self.presented.lock() {
*presented = None;
}
}
fn bind<'a>(
&'a self,
conversation_id: &'a str,
objective: Option<newt_core::PromptId>,
persist: PlanDraftPersist<'a>,
) -> TurnPlanDraftSink<'a> {
TurnPlanDraftSink {
state: self,
conversation_id,
objective,
persist,
}
}
fn take_for_presentation(
&self,
objective: newt_core::PromptId,
) -> Option<newt_core::agentic::PresentedPlan> {
let held = self.latest.lock().ok()?.clone()?;
if held.objective != objective {
return None;
}
let mut presented = self.presented.lock().ok()?;
if presented.as_ref().is_some_and(|p| {
p.objective == objective && p.plan.draft.revision == held.draft.revision
}) {
return None;
}
let plan = newt_core::agentic::PresentedPlan::new(held.draft);
*presented = Some(ObjectivePresented {
objective,
plan: plan.clone(),
});
Some(plan)
}
fn take_approved(
&self,
objective: newt_core::PromptId,
) -> Option<newt_core::agentic::PresentedPlan> {
let mut presented = self.presented.lock().ok()?;
if presented.as_ref()?.objective != objective {
return None;
}
presented.take().map(|p| p.plan)
}
}
struct TurnPlanDraftSink<'a> {
state: &'a PlanDraftState,
conversation_id: &'a str,
objective: Option<newt_core::PromptId>,
persist: PlanDraftPersist<'a>,
}
fn next_plan_draft_revision(
current: Option<&ObjectiveDraft>,
objective: newt_core::PromptId,
) -> u32 {
match current {
Some(held) if held.objective == objective => held.draft.revision + 1,
_ => 1,
}
}
impl newt_core::agentic::PlanDraftSink for TurnPlanDraftSink<'_> {
fn save_draft(&self, markdown: String) -> Result<u32, String> {
let objective = self
.objective
.ok_or_else(|| "plan draft: no active objective to bind to".to_string())?;
let mut latest = self
.state
.latest
.lock()
.map_err(|_| "plan draft lock poisoned".to_string())?;
let revision = next_plan_draft_revision(latest.as_ref(), objective);
let path = newt_core::session_plan_path(self.conversation_id);
(self.persist)(&path, &markdown).map_err(|error| format!("plan draft write: {error}"))?;
*latest = Some(ObjectiveDraft {
objective,
draft: newt_core::agentic::PlanDraft { revision, markdown },
});
Ok(revision)
}
fn latest_draft(&self) -> Option<newt_core::agentic::PlanDraft> {
let held = self.state.latest.lock().ok()?.clone()?;
(Some(held.objective) == self.objective).then_some(held.draft)
}
}
#[derive(Debug, Default)]
struct ConversationModeStates {
auto: AutoModeState,
plan: PlanModeState,
plan_draft: PlanDraftState,
}
impl ConversationModeStates {
fn clear(&self) {
self.auto.clear();
self.plan.clear();
self.plan_draft.clear();
}
}
struct TurnAutoModeControl<'a> {
state: &'a AutoModeState,
conversation_id: &'a str,
}
impl newt_core::agentic::OperatingModeControl for TurnAutoModeControl<'_> {
fn select_operating_mode(&self, requested: &str) -> Result<String, String> {
let Some(mode) = OperatingMode::from_keyword(requested) else {
return Err(
"choose one of: chat, dev, admin, plan, diagnose (full-auto is human-only)"
.to_string(),
);
};
if matches!(mode, OperatingMode::Auto | OperatingMode::FullAuto) {
return Err(format!(
"{} cannot be model-selected; choose chat, dev, admin, plan, or diagnose",
mode.as_str()
));
}
let mut selected = self
.state
.selected
.lock()
.map_err(|_| "session mode state is unavailable".to_string())?;
*selected = Some((self.conversation_id.to_string(), mode));
Ok(format!(
"selected {} for the next action-shaped turn in this conversation. \
The current turn's operating mode, disposition, caveats, and permissions are unchanged.",
mode.as_str()
))
}
}
fn operating_mode_command_lines(
arg: &str,
active: &mut OperatingMode,
) -> Result<Vec<String>, String> {
let arg = arg.trim().to_ascii_lowercase();
if arg.is_empty() || arg == "list" {
let mut lines = vec![format!(
"active operating mode: {} — {}",
active.as_str(),
active.description()
)];
lines.push("available operating modes:".to_string());
lines.extend(
OperatingMode::all()
.iter()
.map(|mode| format!(" {:<9} {}", mode.as_str(), mode.description())),
);
return Ok(lines);
}
if matches!(arg.as_str(), "show" | "status") {
return Ok(vec![format!(
"active operating mode: {} — {}",
active.as_str(),
active.description()
)]);
}
if matches!(arg.as_str(), "off" | "clear" | "reset" | "default") {
*active = OperatingMode::Chat;
return Ok(vec![
"operating mode reset to chat (the default)".to_string()
]);
}
let Some(mode) = OperatingMode::from_keyword(&arg) else {
let names = OperatingMode::all()
.iter()
.map(|mode| mode.as_str())
.collect::<Vec<_>>()
.join(" | ");
return Err(format!("unknown /mode '{arg}' — usage: /mode <{names}>"));
};
*active = mode;
Ok(vec![format!(
"operating mode set to {} — {}",
mode.as_str(),
mode.description()
)])
}
fn effective_operating_mode(
configured: OperatingMode,
intake: &newt_core::agentic::PromptIntake,
model_plan_phase: bool,
auto_selected: Option<OperatingMode>,
) -> OperatingMode {
use newt_core::agentic::PromptDisposition;
if model_plan_phase {
return OperatingMode::Plan;
}
if configured != OperatingMode::Auto {
return match (configured, intake.disposition()) {
(
OperatingMode::Dev | OperatingMode::Admin | OperatingMode::FullAuto,
PromptDisposition::Research,
) => OperatingMode::Diagnose,
(
OperatingMode::Dev | OperatingMode::Admin | OperatingMode::FullAuto,
PromptDisposition::Plan,
) => OperatingMode::Plan,
(
OperatingMode::Dev | OperatingMode::Admin | OperatingMode::FullAuto,
PromptDisposition::Ask | PromptDisposition::Explain,
) => OperatingMode::Chat,
_ => configured,
};
}
let prompt = intake
.atomic_asks()
.iter()
.map(newt_core::agentic::AtomicAsk::text)
.collect::<Vec<_>>()
.join("\n")
.to_ascii_lowercase();
match intake.disposition() {
PromptDisposition::Act => {
if let Some(selected) = auto_selected
.filter(|mode| !matches!(mode, OperatingMode::Auto | OperatingMode::FullAuto))
{
return selected;
}
let mut objective = prompt.trim_start();
for prefix in ["please ", "can you ", "could you ", "would you "] {
if let Some(rest) = objective.strip_prefix(prefix) {
objective = rest.trim_start();
break;
}
}
let starts_any =
|needles: &[&str]| needles.iter().any(|needle| objective.starts_with(needle));
if starts_any(&[
"diagnose ",
"investigate ",
"troubleshoot ",
"find the root cause",
]) {
OperatingMode::Diagnose
} else if starts_any(&["plan ", "write a plan", "make a plan", "formulate a plan"]) {
OperatingMode::Plan
} else if starts_any(&[
"use admin mode",
"work in admin mode",
"act as a sysadmin",
"perform system administration",
]) {
OperatingMode::Admin
} else {
OperatingMode::Dev
}
}
PromptDisposition::Research => OperatingMode::Diagnose,
PromptDisposition::Ask | PromptDisposition::Explain => OperatingMode::Chat,
PromptDisposition::Plan => OperatingMode::Plan,
}
}
fn apply_operating_mode_to_intake(
mode: OperatingMode,
intake: &mut newt_core::agentic::PromptIntake,
) {
match mode {
OperatingMode::Plan => {
intake.enforce_read_only(newt_core::agentic::PromptDisposition::Plan);
}
OperatingMode::Diagnose => {
intake.enforce_read_only(newt_core::agentic::PromptDisposition::Research);
}
_ => {}
}
}
fn operating_mode_caveats(
configured: OperatingMode,
model_plan_phase: bool,
caveats: newt_core::Caveats,
) -> newt_core::Caveats {
let mode = if model_plan_phase {
OperatingMode::Plan
} else {
configured
};
match mode {
OperatingMode::Plan => caveats.meet(&newt_core::agentic::plan_phase_clamp()),
OperatingMode::Diagnose => caveats.meet(&diagnose_mode_clamp()),
_ => caveats,
}
}
fn operating_mode_permission_clamp(
configured: OperatingMode,
model_plan_phase: bool,
posture_clamp: Option<newt_core::Caveats>,
) -> Option<newt_core::Caveats> {
let has_posture_clamp = posture_clamp.is_some();
let ceiling = operating_mode_caveats(
configured,
model_plan_phase,
posture_clamp.unwrap_or_else(newt_core::Caveats::top),
);
(has_posture_clamp || ceiling != newt_core::Caveats::top()).then_some(ceiling)
}
fn diagnose_mode_clamp() -> newt_core::Caveats {
use newt_core::{CountBound, Scope};
newt_core::Caveats {
fs_read: Scope::All,
fs_write: Scope::none(),
exec: Scope::none(),
net: Scope::All,
max_calls: CountBound::Unlimited,
valid_for_generation: Scope::All,
}
}
fn operating_mode_prompt(configured: OperatingMode, effective: OperatingMode) -> String {
let (description, instructions) = match (configured != effective, effective) {
(true, OperatingMode::Diagnose) => (
"Gather evidence and identify the cause.",
"Gather evidence using the tools needed for the assignment under the session grants. \
Report supported findings and continue any already requested follow-through. Ask \
when a consequential decision or required permission remains unresolved.",
),
(true, OperatingMode::Plan) => (
"Organize the requested work into a concrete, sequenced plan.",
"Build a concrete plan using necessary evidence and tools under the session grants. \
Follow the requested scope and any active Plan-phase restrictions. Ask about \
unresolved decisions or required permissions.",
),
_ => (effective.description(), effective.instructions()),
};
let identity = if configured == effective {
format!("Operating mode: {} — {}", effective.as_str(), description)
} else {
format!(
"Configured session mode: {}. Effective working style for this turn: {} — {}.",
configured.as_str(),
effective.as_str(),
description,
)
};
let auto_control = if configured == OperatingMode::Auto {
"\nAuto-mode control: use select_operating_mode when the next action-shaped turn \
should use chat, dev, admin, plan, or diagnose. A selection takes effect only \
on a later turn and grants no authority. Never attempt to select full-auto."
} else {
""
};
let configured_invariants = if configured == OperatingMode::Admin {
"\nConfigured admin invariants remain in force: Do no harm. Make minimal changes. \
Respect privacy. With great power comes great responsibility."
} else {
""
};
format!(
"<operating_mode configured=\"{}\" effective=\"{}\">\n{}\n\
Effective instructions:\n{}{}{}\n\
Inferred styles guide the response; they do not change session grants or \
bypass permission gates. Explicit operator Plan or Diagnose modes and an \
active model Plan phase retain their permission ceilings. Stay within the \
requested task.\n</operating_mode>",
configured.as_str(),
effective.as_str(),
identity,
instructions,
auto_control,
configured_invariants,
)
}
pub(crate) use newt_core::posture::{build_posture, ActivePosture};
fn posture_prompt(posture: &ActivePosture) -> String {
let authority_line = if posture.permission_clamp().is_none() {
format!(
"Active permission posture: {} (no permission clamp)",
posture.name
)
} else {
format!(
"Active permission posture: {} — {}",
posture.name, posture.clamp_summary
)
};
let mut lines = vec![
format!(
"<permission_posture name=\"{}\">",
posture.name.replace('"', """)
),
authority_line,
];
if posture.permission_clamp().is_none() {
lines.push(
"This posture has no configured permission floor. Its skill and framing \
do not grant authority; the session's existing boundaries remain in force."
.to_string(),
);
} else {
lines.push(
"This posture's permission preset is an authority floor. It can only \
narrow permissions and cannot be overridden by the operating mode or \
a session grant."
.to_string(),
);
}
if let Some(framing) = &posture.framing {
lines.push(format!("Posture framing: {framing}"));
}
if let Some(skill) = &posture.skill_body {
lines.push(format!("Preloaded posture skill guidance:\n{skill}"));
}
lines.push("</permission_posture>".to_string());
lines.join("\n")
}
fn session_control_prompt(
configured_mode: OperatingMode,
effective_mode: OperatingMode,
posture: Option<&ActivePosture>,
) -> String {
let mut prompt = operating_mode_prompt(configured_mode, effective_mode);
if let Some(posture) = posture {
prompt.push_str("\n\n");
prompt.push_str(&posture_prompt(posture));
}
prompt
}
fn effective_caveats(
base: &newt_core::Caveats,
posture: Option<&ActivePosture>,
) -> newt_core::Caveats {
match posture.and_then(ActivePosture::permission_clamp) {
Some(clamp) => base.meet(clamp),
None => base.clone(),
}
}
fn meet_persona_caveats(
base: newt_core::Caveats,
_persona: Option<&Persona>,
) -> newt_core::Caveats {
base
}
fn exec_floor_from(
effective_exec: &newt_core::caveats::Scope<String>,
posture_floor_active: bool,
) -> Option<newt_core::caveats::Scope<String>> {
use newt_core::caveats::Scope;
match effective_exec {
Scope::All if !posture_floor_active => None,
scope => Some(scope.clone()),
}
}
pub(crate) fn permissions_command_lines(
state: &PermissionPromptState,
enabled: bool,
log_path: Option<&std::path::Path>,
active_posture: Option<&ActivePosture>,
) -> Vec<String> {
let mut lines = Vec::new();
if let Some(posture) = active_posture {
if posture.permission_clamp().is_none() {
lines.push(format!(
"active permission posture: {} (no permission clamp)",
posture.name
));
} else {
lines.push(format!(
"active permission posture: {} — preset '{}' clamps authority (floor): {}",
posture.name, posture.preset_name, posture.clamp_summary
));
lines.push(
"this clamp WINS over --disable-ocap/--yolo and over session grants (#307)"
.to_string(),
);
}
}
if !enabled {
lines.push(
"permission prompting is OFF (start with --prompt-for-permissions or set \
[tui.permissions] prompt = true)"
.to_string(),
);
}
lines.push(format!(
"session allows: {} · encrypted permanent allows: {} · terminal default: {}",
state.session_allow_count(),
state.durable_grants.len(),
state.terminal_default().as_str()
));
if state.decisions.is_empty() {
lines.push("no prompted permission decisions this session".to_string());
} else {
lines.push("prompted permission decisions this session:".to_string());
for d in &state.decisions {
lines.push(format!(
" {:<5} {:<7} {}:{} via {}",
d.decision, d.scope, d.kind, d.target, d.tool
));
}
}
if let Some(path) = log_path {
lines.push(format!("log: {}", path.display()));
lines.push(
"make session allows permanent in /settings → Permissions (OCAP), or /permissions save. \
The reviewed snapshot is signed and encrypted; the log is never authority. \
The hostname prompt's [A] choice still saves net hosts to TOML config."
.to_string(),
);
}
lines
}
#[cfg(feature = "rich-tui")]
pub(crate) fn permission_panel_lines(
state: &PermissionPromptState,
enabled: bool,
log: Option<&std::path::Path>,
posture: Option<&ActivePosture>,
) -> Vec<String> {
let mut rows = permissions_command_lines(state, enabled, log, posture);
if let Some(path) = log {
rows.push(String::new());
rows.extend(permission_audit_lines(path, 50));
}
rows
}
fn flat_permission_records(body: &str) -> Vec<newt_core::PermissionRecord> {
body.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str(l).ok())
.collect()
}
fn audit_row(rec: &newt_core::PermissionRecord) -> String {
format!(
" {:<7} {:<9} {:<8} {} via {}",
rec.decision, rec.scope, rec.kind, rec.target, rec.tool
)
}
fn ocap_propose_command_lines(
capture_override: Option<PathBuf>,
config: Option<&Path>,
save: bool,
session: Option<&str>,
) -> anyhow::Result<Vec<String>> {
let Some(session) = session else {
return Ok(vec![
"No active session, so there is no session-scoped evidence to propose from. \
`newt ocap propose` (CLI) reviews the whole capture."
.to_string(),
]);
};
let config_path = config
.map(Path::to_path_buf)
.or_else(newt_core::Config::user_config_path)
.ok_or_else(|| anyhow::anyhow!("cannot resolve the newt config directory"))?;
let capture_path = resolve_ocap_capture_path(capture_override, &config_path);
let capture_text = match std::fs::read_to_string(&capture_path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(vec![
format!("No flight-recorder capture at {}.", capture_path.display()),
"Arm the recorder by running a task under `--full-access` (or set \
`$NEWT_FLIGHT_RECORDER`); then `/ocap propose` turns that observed authority into a \
reviewable proposal."
.to_string(),
]);
}
Err(e) => return Err(anyhow::anyhow!("read {}: {e}", capture_path.display())),
};
let store_dir = config_path.with_file_name("ocap");
let store_files: Vec<(newt_core::ocap_store::Verdict, Option<String>)> =
newt_core::ocap_store::VERDICTS
.iter()
.map(|&v| {
(
v,
std::fs::read_to_string(store_dir.join(v.filename())).ok(),
)
})
.collect();
let approve_path = store_dir.join(newt_core::ocap_store::Verdict::Approve.filename());
let existing = match std::fs::read_to_string(&approve_path) {
Ok(t) => Some(t),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => return Err(anyhow::anyhow!("read {}: {e}", approve_path.display())),
};
let now = crate::probe::today_local_date();
let (merged, proposal) = newt_core::ocap_propose::propose_for(
&capture_text,
&store_files,
existing.as_deref(),
crate::ocap_high_danger_predicate(),
&now,
Some(session),
)
.map_err(|e| anyhow::anyhow!("propose: {e}"))?;
if proposal.is_empty() {
return Ok(vec![
"This session's capture is fully accounted for by the current policy — nothing to propose."
.to_string(),
]);
}
let wrote = save && proposal.additions_len() > 0;
if wrote {
let merged_toml = merged
.to_toml()
.map_err(|e| anyhow::anyhow!("serialize approve.toml: {e}"))?;
std::fs::create_dir_all(&store_dir)
.map_err(|e| anyhow::anyhow!("create {}: {e}", store_dir.display()))?;
std::fs::write(&approve_path, &merged_toml)
.map_err(|e| anyhow::anyhow!("write {}: {e}", approve_path.display()))?;
}
Ok(render_ocap_proposal(&proposal, wrote))
}
pub(crate) const OCAP_USAGE: &str = "usage: /ocap propose show this session's grant candidates\n /ocap propose save also write them, unsigned, to approve.toml";
pub(crate) fn parse_ocap_command(slash_body: &str) -> Option<bool> {
let words: Vec<&str> = slash_body.split_whitespace().collect();
match words.as_slice() {
["ocap", "propose"] => Some(false),
["ocap", "propose", "save"] => Some(true),
_ => None,
}
}
fn note_of(note: &Option<String>) -> &str {
note.as_deref().unwrap_or("observed")
}
fn render_ocap_proposal(proposal: &newt_core::ocap_propose::Proposal, wrote: bool) -> Vec<String> {
let mut out = Vec::new();
out.push(format!(
"Proposed {} durable candidate(s) from this session's capture:",
proposal.additions_len()
));
for e in &proposal.additions.exec {
out.push(format!(" exec {} [{}]", e.target, note_of(&e.note)));
}
for e in &proposal.additions.fs {
let mode = if e.write { "rw" } else { "ro" };
out.push(format!(
" fs {} ({mode}) [{}]",
e.path,
note_of(&e.note)
));
}
for e in &proposal.additions.net {
out.push(format!(" net {} [{}]", e.host, note_of(&e.note)));
}
out.push(String::new());
for d in &proposal.deferred {
out.push(format!(" deferred (review, high danger): {}", d.class));
out.push(format!(
" {} — {}",
d.command,
d.reason.trim_start_matches("observed: ").trim()
));
}
out.push(String::new());
if wrote {
out.push(String::from(
"Wrote the low-danger candidates to the approve store (UNSIGNED — \
these grants are no-ops until you `newt doctor --sign-ocap`).",
));
} else {
out.push(String::from(
"This is a reviewable proposal only. To record the low-danger candidates \
UNSIGNED, run `/ocap propose save`, then `newt doctor --sign-ocap` to bless them.",
));
}
out
}
fn resolve_ocap_capture_path(capture: Option<PathBuf>, config_path: &Path) -> PathBuf {
if let Some(p) = capture {
return p;
}
if let Some(v) = std::env::var_os(newt_core::flight_recorder::CAPTURE_PATH_ENV) {
let s = v.to_string_lossy();
if !(s.eq_ignore_ascii_case("off") || s == "0") {
return PathBuf::from(v);
}
}
config_path
.with_file_name("flight-recorder")
.join("unconfined.jsonl")
}
fn push_unchained(out: &mut Vec<String>, records: &[newt_core::PermissionRecord], limit: usize) {
let show = if limit == 0 { records.len() } else { limit };
for rec in records.iter().take(show) {
out.push(format!("{} (unchained)", audit_row(rec)));
}
}
fn push_chain_breaks(
out: &mut Vec<String>,
breaks: &[newt_core::permission_journal::ChainBreak],
anchored: bool,
) {
use newt_core::permission_journal::ChainBreak;
if breaks.is_empty() {
return;
}
let anchor = if anchored {
"anchored to the stored head"
} else {
"no stored head ref, so removal from the END was NOT checked"
};
out.push(format!(
"!! CHAIN BROKEN — this permission log does not verify ({} problem(s); {anchor}).",
breaks.len()
));
for br in breaks {
out.push(match br {
ChainBreak::Edited { index } => {
format!("!! record {index}: edited — its bytes no longer match its own address")
}
ChainBreak::Unreadable { index } => {
format!("!! record {index}: its address does not parse")
}
ChainBreak::BrokenLink { index } => format!(
"!! record {index}: broken link — a record before it was deleted or reordered"
),
ChainBreak::NotAChain { index } => {
format!("!! record {index}: not a single-parent chain link")
}
ChainBreak::Truncated { expected_head } => format!(
"!! truncated — the chain no longer reaches the stored head {expected_head}"
),
});
}
}
pub(crate) fn permission_audit_lines(log_path: &std::path::Path, limit: usize) -> Vec<String> {
let body = match std::fs::read_to_string(log_path) {
Ok(body) => body,
Err(e) => {
return vec![format!(
"unable to read permission log {}: {e}",
log_path.display()
)];
}
};
let mut out = Vec::new();
let chained = newt_core::permission_journal::read_jsonl(&body);
if chained.is_empty() && !body.trim().is_empty() {
let unchained = flat_permission_records(&body);
if !unchained.is_empty() {
out.push(
"pre-chain migration: this log has not been rotated onto the chain yet — \
the lines below carry no integrity guarantee"
.to_string(),
);
push_unchained(&mut out, &unchained, limit);
return out;
}
}
let raw_lines = body.lines().filter(|l| !l.trim().is_empty()).count();
let unparseable = raw_lines.saturating_sub(chained.len());
let head = newt_core::permission_journal::read_head(log_path);
let mut breaks = newt_core::permission_journal::verify_chain(&chained, head.as_deref());
breaks.extend((0..unparseable).map(|_| {
newt_core::permission_journal::ChainBreak::Unreadable {
index: chained.len(),
}
}));
push_chain_breaks(&mut out, &breaks, head.is_some());
let pre_chain_path = newt_core::permission_journal::pre_chain_path(log_path);
if let Ok(pre_body) = std::fs::read_to_string(&pre_chain_path) {
let unchained = flat_permission_records(&pre_body);
if !unchained.is_empty() {
out.push(format!(
"pre-chain migration: {} holds the earlier lines",
pre_chain_path.display()
));
push_unchained(&mut out, &unchained, limit);
}
}
let records = newt_core::permission_journal::records(&chained);
if records.is_empty() {
if out.is_empty() {
out.push("no permission log entries yet".to_string());
}
return out;
}
let show = if limit == 0 { records.len() } else { limit };
let shown = records.len().min(show);
out.push(format!(
"permission audit: {shown} of {} (newest first)",
records.len()
));
for rec in records.iter().rev().take(show) {
out.push(audit_row(rec));
}
out
}
fn ocap_disabled_banner() -> String {
"⚠ ocap DISABLED (--disable-ocap): permitted commands may run unconfined on the \
host shell; active exec floors can force confinement or denial — fs tools keep \
the workspace fence; drop the flag to restore default confinement (#297)"
.to_string()
}
fn ocap_disabled_record(conversation_id: &str) -> newt_core::PermissionRecord {
newt_core::PermissionRecord::new(
conversation_id,
"run_command",
newt_core::DenialKind::Exec,
"*",
"ocap-disabled",
"session",
)
}
fn full_access_banner() -> String {
"⚠ FULL ACCESS (--full-access): the agent is granted your full AMBIENT authority \
for this run — the Object-Capability attenuations (fs fence, net leash, exec \
allowlist) are lifted and run_command uses the `host` shell engine (a real \
/bin/sh inside the platform kernel jail). Writes are still prompted. Drop the \
flag to restore Object-Capability authority restrictions. Private HTTP destinations \
still require exact MCP net grants in [tui.permissions] net."
.to_string()
}
fn full_access_record(conversation_id: &str) -> newt_core::PermissionRecord {
newt_core::PermissionRecord::new(
conversation_id,
"session",
newt_core::DenialKind::Exec,
"*",
"full-access",
"session",
)
}
fn ocap_store_folded_record(
conversation_id: &str,
folded_count: usize,
) -> newt_core::PermissionRecord {
newt_core::PermissionRecord::new(
conversation_id,
"session",
newt_core::DenialKind::Exec,
&format!("{folded_count} durable grant(s)"),
"allow",
"durable-store",
)
}
#[cfg(test)]
pub(crate) mod test_env_guard {
use newt_core::process_env::{lock, EnvGuard};
pub(crate) fn env_read_guard() -> EnvGuard {
lock()
}
pub(crate) async fn env_read_guard_async() -> EnvGuard {
lock()
}
pub(crate) fn env_write_guard() -> EnvGuard {
lock()
}
#[cfg(unix)]
pub(crate) async fn env_write_guard_async() -> EnvGuard {
lock()
}
}
#[cfg(test)]
#[path = "lib_tests/process_env_isolation_tests.rs"]
mod process_env_isolation_tests;
#[cfg(test)]
#[path = "lib_tests/caveat_policy_tests.rs"]
mod caveat_policy_tests;
struct ManagerNoteSink<'m> {
memory: &'m mut newt_core::MemoryManager,
}
impl newt_core::NoteSink for ManagerNoteSink<'_> {
fn add(&mut self, fact: &str) -> anyhow::Result<()> {
self.memory.add_note(fact)
}
fn replace(&mut self, old_substring: &str, new_text: &str) -> anyhow::Result<()> {
self.memory.replace_note(old_substring, new_text)
}
fn remove(&mut self, substring: &str) -> anyhow::Result<()> {
self.memory.remove_note(substring)
}
fn usage_line(&self) -> String {
self.memory
.usage()
.iter()
.find(|(label, _, _)| label == "notes")
.map(|(_, cur, max)| {
let pct = if *max > 0 { cur * 100 / max } else { 0 };
format!("notes: {cur}/{max} chars ({pct}%)")
})
.unwrap_or_else(|| "notes: usage unavailable".to_string())
}
}
#[derive(Clone)]
struct SummarizerOpts {
num_ctx: Option<u32>,
keep_alive: String,
timeout_secs: u64,
retries: u32,
fallback_model: Option<String>,
color: bool,
caps: newt_core::tty::LineCaps,
run_allowance: Option<std::sync::Arc<newt_core::agentic::run_allowance::RunAllowance>>,
}
impl Default for SummarizerOpts {
fn default() -> Self {
Self {
num_ctx: None,
keep_alive: "5m".to_string(),
timeout_secs: 60,
retries: 2,
fallback_model: None,
color: false,
caps: newt_core::tty::LineCaps::None,
run_allowance: None,
}
}
}
fn retry_progress_msg(attempt: u32, total: u32) -> String {
format!("↻ summarizer retrying (attempt {attempt}/{total})…")
}
fn fallback_progress_msg(model: &str) -> String {
format!("⚠ summarizer falling back to {model}…")
}
fn failure_progress_msg(err: &anyhow::Error) -> String {
format!("⚠ summarizer failed ({err}); using static compression marker…")
}
fn summarizer_notice(msg: String) -> newt_core::tty::Notice<'static> {
newt_core::tty::Notice::new(newt_core::tty::Level::Warn, "", msg)
}
async fn summarize_attempt(
client: &reqwest::Client,
chat_url: &str,
body: &serde_json::Value,
api_key: &Option<String>,
openai: bool,
) -> anyhow::Result<(String, Option<newt_core::TokenUsage>)> {
let mut req = client.post(chat_url).json(body);
if let Some(key) = api_key {
req = req.bearer_auth(key);
}
let resp = req.send().await?;
if !resp.status().is_success() {
anyhow::bail!("summarizer endpoint {}", resp.status());
}
let json: serde_json::Value = resp.json().await?;
extract_summary(&json, openai)
}
fn extract_summary(
json: &serde_json::Value,
openai: bool,
) -> anyhow::Result<(String, Option<newt_core::TokenUsage>)> {
let raw = if openai {
json["choices"][0]["message"]["content"].as_str()
} else {
json["message"]["content"].as_str()
}
.unwrap_or("");
let (clean, _reasoning) = newt_core::split_reasoning(raw);
if clean.trim().is_empty() {
anyhow::bail!("summarizer returned empty content (thinking-only reply?)");
}
let usage = if openai {
newt_core::agentic::openai_usage(&json["usage"])
} else {
newt_core::agentic::ollama_usage(json)
};
Ok((clean, usage))
}
#[cfg(test)]
#[path = "lib_tests/summarizer_extract_tests.rs"]
mod summarizer_extract_tests;
async fn summarize_one_model(
url: &str,
model: &str,
openai: bool,
prompt: &str,
opts: &SummarizerOpts,
api_key: &Option<String>,
) -> anyhow::Result<(String, Option<newt_core::TokenUsage>)> {
let chat_url = if openai {
format!("{}/v1/chat/completions", url.trim_end_matches('/'))
} else {
format!("{}/api/chat", url.trim_end_matches('/'))
};
if !openai {
let warm_url = format!("{}/api/generate", url.trim_end_matches('/'));
let warm_body = serde_json::json!({
"model": model,
"keep_alive": opts.keep_alive,
"stream": false,
});
if let Ok(warm_client) = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(600))
.build()
{
let mut wreq = warm_client.post(&warm_url).json(&warm_body);
if let Some(key) = api_key {
wreq = wreq.bearer_auth(key);
}
let _ = wreq.send().await;
}
}
let body = if openai {
serde_json::json!({
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": false,
})
} else {
let mut b = serde_json::json!({
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": false,
"think": false,
"keep_alive": opts.keep_alive,
});
if let Some(ctx_size) = opts.num_ctx {
b["options"] = serde_json::json!({ "num_ctx": ctx_size });
}
b
};
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(opts.timeout_secs))
.build()?;
let mut last_err: Option<anyhow::Error> = None;
for attempt in 0..=opts.retries {
if attempt > 0 {
summarizer_notice(retry_progress_msg(attempt + 1, opts.retries + 1)).emit(
opts.caps,
newt_core::tty::Sink::Stdout,
opts.color,
);
let backoff = std::time::Duration::from_millis(250u64 << (attempt - 1).min(4));
tokio::time::sleep(backoff).await;
}
if let Some(allowance) = &opts.run_allowance {
allowance.try_reserve()?;
}
match summarize_attempt(&client, &chat_url, &body, api_key, openai).await {
Ok(s) => return Ok(s),
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("summarizer failed")))
}
fn make_loop_summarizer(
url: String,
model: String,
kind: newt_core::BackendKind,
api_key: Option<String>,
model_path: Option<String>,
opts: SummarizerOpts,
) -> newt_core::Summarizer {
if kind == newt_core::BackendKind::Embedded {
return make_embedded_summarizer(model, model_path, opts.timeout_secs);
}
Box::new(move |prompt: String| {
let url = url.clone();
let model = model.clone();
let api_key = api_key.clone();
let opts = opts.clone();
let openai = kind == newt_core::BackendKind::Openai;
Box::pin(async move {
match summarize_one_model(&url, &model, openai, &prompt, &opts, &api_key).await {
Ok(s) => Ok(s),
Err(primary_err) => {
match opts.fallback_model.clone() {
Some(fb) if fb != model => {
summarizer_notice(fallback_progress_msg(&fb)).emit(
opts.caps,
newt_core::tty::Sink::Stdout,
opts.color,
);
summarize_one_model(&url, &fb, openai, &prompt, &opts, &api_key)
.await
.map_err(|fallback_err| {
anyhow::anyhow!(
"primary summarizer failed: {primary_err}; fallback summarizer '{fb}' failed: {fallback_err}"
)
})
}
_ => {
summarizer_notice(failure_progress_msg(&primary_err)).emit(
opts.caps,
newt_core::tty::Sink::Stdout,
opts.color,
);
Err(primary_err)
}
}
}
}
})
})
}
fn failing_summarizer(msg: String) -> newt_core::Summarizer {
Box::new(move |_prompt: String| {
let msg = msg.clone();
Box::pin(async move { Err(anyhow::anyhow!(msg)) })
})
}
#[cfg_attr(not(feature = "embedded"), allow(unused_variables))]
fn make_embedded_summarizer(
model: String,
model_path: Option<String>,
timeout_secs: u64,
) -> newt_core::Summarizer {
#[cfg(feature = "embedded")]
{
let Some(path) = model_path else {
return failing_summarizer(
"summarizer kind=embedded needs `summarizer.model_path` (the local GGUF)"
.to_string(),
);
};
match newt_inference::embedded::EmbeddedBackend::new(&model, &path) {
Ok(backend) => {
let backend = std::sync::Arc::new(backend);
Box::new(move |prompt: String| {
let backend = backend.clone();
Box::pin(async move {
let req = newt_inference::ChatRequest {
messages: vec![newt_inference::backend::Message::user(prompt)],
max_tokens: Some(1024),
};
backend
.complete_with_timeout(
req,
std::time::Duration::from_secs(timeout_secs),
)
.await
.map(|reply| (reply.content, reply.usage))
})
})
}
Err(e) => failing_summarizer(format!("embedded summarizer init failed: {e}")),
}
}
#[cfg(not(feature = "embedded"))]
{
failing_summarizer(
"summarizer kind=embedded, but this build lacks the `embedded` feature — \
rebuild with --features embedded"
.to_string(),
)
}
}
struct FailingEmbedder {
msg: String,
}
#[async_trait::async_trait]
impl newt_core::Embedder for FailingEmbedder {
async fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
Err(anyhow::anyhow!(self.msg.clone()))
}
}
fn embeddings_backend_is_embedded(cfg: &newt_core::SemanticConfig) -> bool {
cfg.embeddings_api == Some(newt_core::BackendKind::Embedded)
|| (cfg.embeddings_api.is_none() && cfg.embeddings_endpoint.is_none())
}
fn effective_embedding_model_path(
explicit: Option<String>,
default_present: Option<std::path::PathBuf>,
) -> Option<String> {
explicit.or_else(|| default_present.map(|d| d.to_string_lossy().into_owned()))
}
fn build_semantic_embedder(
semantic_cfg: &newt_core::SemanticConfig,
inf_url: &str,
inf_kind: newt_core::BackendKind,
inf_key: Option<&str>,
) -> Box<dyn newt_core::Embedder> {
if embeddings_backend_is_embedded(semantic_cfg) {
return make_embedded_embedder(
semantic_cfg.embedding_model.clone(),
semantic_cfg.embedding_model_path.clone(),
);
}
let (emb_url, emb_kind, emb_key) =
resolve_embeddings_target(semantic_cfg, inf_url, inf_kind, inf_key);
Box::new(newt_core::EmbeddingsClient::new(
emb_url,
semantic_cfg.embedding_model.clone(),
emb_kind,
emb_key,
60,
2,
))
}
fn semantic_embedder_unavailable_reason(cfg: &newt_core::SemanticConfig) -> Option<String> {
if !embeddings_backend_is_embedded(cfg) {
return None;
}
#[cfg(not(feature = "embedded"))]
{
Some(
"embedded semantic retrieval is selected, but this binary lacks the `embedded` \
feature; rebuild with --features embedded or configure an explicit \
[context.semantic].embeddings_endpoint"
.to_string(),
)
}
#[cfg(feature = "embedded")]
{
let Some(path) = cfg.embedding_model_path.as_deref() else {
return Some(
"embedded semantic retrieval has no model — run `newt models pull-embed` to \
fetch the default on-host model (bge-small), or set \
[context.semantic].embedding_model_path to a local candle-clean standard-BERT dir"
.to_string(),
);
};
let dir = std::path::Path::new(path);
if !dir.is_dir() {
return Some(format!(
"embedded semantic retrieval model dir not found: {}",
dir.display()
));
}
for required in ["config.json", "tokenizer.json", "model.safetensors"] {
let file = dir.join(required);
if !file.exists() {
return Some(format!(
"embedded semantic retrieval model file not found: {}",
file.display()
));
}
}
None
}
}
#[cfg_attr(not(feature = "embedded"), allow(unused_variables))]
fn make_embedded_embedder(
model: String,
model_path: Option<String>,
) -> Box<dyn newt_core::Embedder> {
#[cfg(feature = "embedded")]
{
let Some(path) = model_path else {
return Box::new(FailingEmbedder {
msg: "embedded semantic retrieval has no model — run `newt models pull-embed` \
(fetches bge-small), or set `[context.semantic].embedding_model_path` to a \
local candle-clean standard-BERT model dir"
.to_string(),
});
};
match newt_inference::embed::CandleEmbedder::new(&model, &path) {
Ok(e) => Box::new(e),
Err(err) => Box::new(FailingEmbedder {
msg: format!("embedded embedder init failed: {err}"),
}),
}
}
#[cfg(not(feature = "embedded"))]
{
Box::new(FailingEmbedder {
msg: "embeddings_api=embedded, but this build lacks the `embedded` feature — \
rebuild with --features embedded"
.to_string(),
})
}
}
fn semantic_zero_index_hint(cfg: &newt_core::SemanticConfig) -> String {
if embeddings_backend_is_embedded(cfg) {
return "semantic: indexed 0 chunks — no embedding model; run `newt models pull-embed` \
to fetch the default on-host model (bge-small), or set \
[context.semantic].embedding_model_path to a local candle-clean standard-BERT \
model dir (retrieval is a no-op until embeddings work)"
.to_string();
}
let target = cfg
.embeddings_endpoint
.as_deref()
.unwrap_or("the active chat backend");
format!(
"semantic: indexed 0 chunks — embeddings from {target} produced no vectors for '{}'; \
configure [context.semantic].embeddings_endpoint/embeddings_api for an Ollama/OpenAI \
embeddings service, or set embedding_model_path to use embedded embeddings \
(retrieval is a no-op until embeddings work)",
cfg.embedding_model
)
}
const EXTRACTED_NOTE_PREFIX: &str = "(auto-extracted) ";
const EXTRACTION_MSG_CHAR_CAP: usize = 2_000;
fn build_extraction_prompt(transcript: &str) -> String {
format!(
"This coding-agent conversation is closing. From the transcript below, \
extract at most 3 durable facts worth remembering in future \
conversations — decisions made, constraints discovered, preferences \
stated. Reply with one short bullet per fact, each line starting with \
\"- \". Do NOT record task progress or anything only meaningful to \
this session. If nothing qualifies, reply with exactly NONE.\
\n\nTranscript:\n{transcript}"
)
}
fn render_extraction_transcript(messages: &[newt_core::MemMessage]) -> Option<String> {
let json_msgs: Vec<serde_json::Value> = messages
.iter()
.filter(|m| m.role != newt_core::Role::System && !m.content.trim().is_empty())
.map(|m| serde_json::json!({"role": m.role.as_str(), "content": m.content}))
.collect();
if json_msgs.is_empty() {
return None;
}
let rendered = newt_core::trim_for_summary(&json_msgs, 2, 6)
.iter()
.map(|m| {
let role = m["role"].as_str().unwrap_or("user");
let content = m["content"].as_str().unwrap_or_default();
let clipped: String = content.chars().take(EXTRACTION_MSG_CHAR_CAP).collect();
let marker = if content.chars().count() > EXTRACTION_MSG_CHAR_CAP {
" [clipped]"
} else {
""
};
format!("{role}: {clipped}{marker}")
})
.collect::<Vec<_>>()
.join("\n");
Some(rendered)
}
fn parse_extraction_bullets(reply: &str) -> Vec<String> {
if reply.trim().eq_ignore_ascii_case("none") {
return Vec::new();
}
reply
.lines()
.filter_map(|line| {
let line = line.trim();
["- ", "* ", "• "].iter().find_map(|p| line.strip_prefix(p))
})
.map(str::trim)
.filter(|s| !s.is_empty() && !s.eq_ignore_ascii_case("none"))
.take(3)
.map(String::from)
.collect()
}
fn should_extract_on_close(enabled: bool, ephemeral: bool, turns: usize) -> bool {
enabled && !ephemeral && turns > 0
}
pub(crate) fn is_ephemeral_session() -> bool {
std::env::var_os("NEWT_EPHEMERAL").is_some()
}
fn close_extraction_notice(saved: usize, rejected: usize) -> String {
let noun = if saved == 1 { "note" } else { "notes" };
if rejected > 0 {
format!("extracted {saved} {noun} on close ({rejected} rejected)")
} else {
format!("extracted {saved} {noun} on close")
}
}
async fn run_close_extraction(
enabled: bool,
ephemeral: bool,
turns: usize,
memory: &mut newt_core::MemoryManager,
complete: &newt_core::Summarizer,
) -> Option<String> {
if !should_extract_on_close(enabled, ephemeral, turns) {
return None;
}
let transcript = render_extraction_transcript(&memory.build_messages("", ""))?;
let reply = match complete(build_extraction_prompt(&transcript)).await {
Ok((r, _usage)) => r,
Err(e) => {
tracing::warn!(error = %e, "close-time note extraction failed — moving on");
return None;
}
};
let bullets = parse_extraction_bullets(&reply);
if bullets.is_empty() {
return None;
}
let (mut saved, mut rejected) = (0usize, 0usize);
for bullet in &bullets {
match memory.add_note(&format!("{EXTRACTED_NOTE_PREFIX}{bullet}")) {
Ok(()) => saved += 1,
Err(e) => {
rejected += 1;
tracing::warn!(error = %e, "extracted note rejected — dropped");
}
}
}
Some(close_extraction_notice(saved, rejected))
}
fn announce_profile(
name: &str,
profile: &newt_core::config::ProfileConfig,
via: &newt_core::config::PickVia,
color: bool,
) {
let techs = if profile.techniques.is_empty() {
"no techniques".to_string()
} else {
profile.techniques.join(", ")
};
let source = match via {
newt_core::config::PickVia::Profile => String::new(),
newt_core::config::PickVia::Bundle(b) => format!(" (via bundle '{b}')"),
newt_core::config::PickVia::InferredBundle(b) => format!(" (via bundle '{b}', inferred)"),
};
if color {
let _ = execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkGrey),
Print(format!("▸ profile '{name}' — {techs}{source}\n")),
ResetColor,
);
} else {
println!("▸ profile '{name}' — {techs}{source}");
}
}
struct LoadoutView<'a> {
name: Option<&'a str>,
loadout: Option<&'a newt_core::Loadout>,
inf_url: &'a str,
inf_model: &'a str,
profile_pick: Option<&'a newt_core::config::ProfilePick>,
persona: Option<&'a str>,
}
impl LoadoutView<'_> {
fn render(&self) -> String {
let mut lines: Vec<String> = Vec::new();
match self.name {
Some(n) => lines.push(format!("Active loadout: {n}")),
None => lines.push("No loadout active.".to_string()),
}
if let Some(l) = self.loadout {
lines.push(" declared:".to_string());
for (label, val) in [
("provider", l.provider.as_deref()),
("model", l.model.as_deref()),
("kit", l.kit.as_deref()),
("profile", l.profile.as_deref()),
("role", l.role.as_deref()),
] {
if let Some(v) = val {
lines.push(format!(" {label:<9}{v}"));
}
}
if let Some(s) = &l.settings {
if let Some(n) = s.num_ctx {
lines.push(format!(" {:<9}{n}", "num_ctx"));
}
if let Some(f) = &s.framing {
lines.push(format!(" {:<9}{f}", "framing"));
}
}
} else if let Some(n) = self.name {
lines.push(format!(" (no [loadouts.{n}] in config)"));
}
lines.push(" resolved:".to_string());
lines.push(format!(
" {:<9}{} @ {}",
"backend", self.inf_model, self.inf_url
));
let profile_line = match self.profile_pick {
Some(p) => {
let via = match &p.via {
newt_core::config::PickVia::Profile => String::new(),
newt_core::config::PickVia::Bundle(b) => format!(" (via bundle '{b}')"),
newt_core::config::PickVia::InferredBundle(b) => {
format!(" (via bundle '{b}', inferred)")
}
};
format!("{}{}", p.name, via)
}
None => "(none)".to_string(),
};
lines.push(format!(" {:<9}{profile_line}", "profile"));
lines.push(format!(
" {:<9}{}",
"persona",
self.persona.unwrap_or("(none)")
));
lines.join("\n")
}
}
fn verify_gate_summary(
workspace: &str,
mode: newt_core::verify_gate::SurfaceMatch,
) -> Option<String> {
let ws = std::path::Path::new(workspace);
let manifest = newt_core::ffi_manifest::FfiManifest::from_workspace(ws).ok()?;
if manifest.is_empty() {
return None; }
let report =
newt_core::verify_gate::gate_python_workspace_with(ws, &manifest.known_modules(), mode)
.ok()?;
if report.accept() {
return None;
}
let mut s = format!(
"verify_gate: {} file(s) import modules not in the workspace surface (the real paths \
are in the system prompt):",
report.revert_set().len()
);
for f in &report.files {
if f.is_clean() {
continue;
}
let mods: Vec<&str> = f.fabrications.iter().map(|x| x.module.as_str()).collect();
s.push_str(&format!(
"\n {} [{}]",
f.path.display(),
mods.join(", ")
));
}
Some(s)
}
struct RevertAction {
banner: String,
corrective: String,
reverted: Vec<std::path::PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RetryStep {
Reprompt,
GiveUp,
}
fn retry_step(budget: u32) -> RetryStep {
if budget > 0 {
RetryStep::Reprompt
} else {
RetryStep::GiveUp
}
}
async fn retry_revert(
workspace: &str,
mode: newt_core::verify_gate::SurfaceMatch,
ledger: &std::cell::RefCell<newt_core::verify_gate::WriteLedger>,
) -> Option<RevertAction> {
use newt_core::verify_gate::{
corrective_prompt, gate_python_workspace_with, revert_only, RetrySurface,
};
let ws = std::path::Path::new(workspace);
let manifest = newt_core::ffi_manifest::FfiManifest::from_workspace(ws).ok()?;
if manifest.is_empty() {
return None; }
let modules = manifest.known_modules();
let report = gate_python_workspace_with(ws, &modules, mode).ok()?;
if report.accept() {
return None;
}
let mods_by_path: std::collections::BTreeMap<std::path::PathBuf, String> = report
.files
.iter()
.filter(|f| !f.is_clean())
.map(|f| {
let mods: Vec<&str> = f.fabrications.iter().map(|x| x.module.as_str()).collect();
(f.path.clone(), mods.join(", "))
})
.collect();
let block = manifest.render_block();
let corrective = corrective_prompt(&report, &block);
let surface = RetrySurface {
modules: &modules,
mode,
block: &block,
};
let outcome = revert_only(ws, &surface, ledger, report).await.ok()?;
if outcome.reverted.is_empty() {
return None; }
let mut detail = String::new();
for p in &outcome.reverted {
let mods = mods_by_path.get(p).map(String::as_str).unwrap_or("");
detail.push_str(&format!("\n {} [{}]", p.display(), mods));
}
let banner = format!(
"retry: reverted {} file(s) newt wrote this turn to pre-turn state:{detail}",
outcome.reverted.len()
);
Some(RevertAction {
banner,
corrective,
reverted: outcome.reverted,
})
}
#[derive(Debug)]
pub(crate) struct BackendChoice {
pub(crate) name: String,
pub(crate) url: String,
pub(crate) declared_model: Option<String>,
pub(crate) requested_model: Option<String>,
pub(crate) declared_serving: Option<newt_core::Serving>,
pub(crate) requested_serving: Option<newt_core::Serving>,
pub(crate) configured_kind: Option<newt_core::BackendKind>,
pub(crate) configured_api: Option<newt_core::OpenAiApi>,
pub(crate) managed: Option<newt_core::ManagedMode>,
pub(crate) api_key: Option<String>,
pub(crate) model_path: Option<String>,
pub(crate) active_model: Option<String>,
pub(crate) observed_serving: Option<newt_core::Serving>,
pub(crate) adopted_serving: Option<newt_core::Serving>,
pub(crate) kind: newt_core::BackendKind,
pub(crate) kind_needs_probe: bool,
pub(crate) api: newt_core::OpenAiApi,
pub(crate) api_needs_probe: bool,
pub(crate) context_window: Option<u32>,
pub(crate) capabilities: newt_core::model_card::ResolvedCapabilities,
pub(crate) card_resolution_error: Option<String>,
pub(crate) notices: CardNotices,
}
impl BackendChoice {
pub(crate) fn synthesized(
name: &str,
url: String,
kind: newt_core::BackendKind,
active_model: Option<String>,
) -> Self {
Self {
name: name.to_string(),
url,
declared_model: None,
requested_model: None,
declared_serving: None,
requested_serving: None,
observed_serving: None,
configured_kind: None,
configured_api: None,
managed: None,
api_key: None,
model_path: None,
active_model: active_model.filter(|m| !m.trim().is_empty()),
adopted_serving: None,
kind,
kind_needs_probe: false,
api: newt_core::OpenAiApi::default(),
api_needs_probe: false,
context_window: None,
capabilities: newt_core::model_card::ResolvedCapabilities::none(),
card_resolution_error: None,
notices: CardNotices::default(),
}
}
pub(crate) fn route_serving(&self) -> Option<newt_core::Serving> {
self.adopted_serving
.or(self.requested_serving)
.or(self.observed_serving)
.or(self.declared_serving)
}
pub(crate) fn route_destination(&self) -> newt_core::BackendDestination {
newt_core::BackendDestination::new(Some(self.url.clone()), self.model_path.clone())
}
pub(crate) fn display_model(&self) -> &str {
self.active_model
.as_deref()
.or(self.declared_model.as_deref())
.unwrap_or("(server decides)")
}
pub(crate) fn principal(&self) -> newt_core::model_card::ServingPrincipal<'_> {
use newt_core::model_card::ServingPrincipal as P;
match self.route_serving() {
Some(newt_core::Serving::Instance) => P::Instance,
Some(newt_core::Serving::Multiplexer) => {
match self
.active_model
.as_deref()
.filter(|m| !m.trim().is_empty())
{
Some(m) => P::MultiplexerModel(m),
None => P::Unknown,
}
}
None if self.url.is_empty()
&& self.model_path.as_deref().is_some_and(|p| !p.is_empty()) =>
{
P::Instance
}
None => match self
.requested_model
.as_deref()
.or(self.declared_model.as_deref())
.filter(|m| !m.trim().is_empty())
{
Some(m) => P::SelectedModel(m),
None => P::Unknown,
},
}
}
pub(crate) fn capability_decision(&self) -> newt_core::model_card::CapabilityDecision {
self.capabilities
.for_route(&self.route_destination(), self.principal())
}
pub(crate) fn card_notice_lines(&mut self) -> Vec<String> {
let now = self.capability_decision().applicability().clone();
let (next, lines) =
notice_transition(&self.notices, self.card_resolution_error.as_deref(), &now);
self.notices = next;
lines
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct CardNotices {
last_error: Option<String>,
last_applicability: Option<newt_core::model_card::CardApplicability>,
}
fn notice_transition(
prev: &CardNotices,
error: Option<&str>,
now: &newt_core::model_card::CardApplicability,
) -> (CardNotices, Vec<String>) {
use newt_core::model_card::CardApplicability as A;
let mut lines = Vec::new();
if let Some(e) = error {
if prev.last_error.as_deref() != Some(e) {
lines.push(e.to_string());
}
} else if prev.last_error.is_some() {
lines.push("card resolution recovered — the configured card resolves again".to_string());
}
if prev.last_applicability.as_ref() != Some(now) {
match (prev.last_applicability.as_ref(), now) {
(Some(A::Active { card: before }), A::Active { card: after }) if before != after => {
lines.push(format!(
"card switched: `{before}` → `{after}` — card-derived behavior now \
follows `{after}`"
));
}
(
Some(A::InactiveModel { .. } | A::InactiveDestination { .. } | A::Undecided { .. }),
A::Active { card },
) => {
lines.push(format!(
"card `{card}` applies again — the session is serving its bound model"
));
}
(Some(A::None), A::Active { card }) => {
lines.push(format!(
"card `{card}` now applies — card-derived behavior follows it"
));
}
(None, A::Active { .. }) => {}
(Some(prev_state), A::None) if !matches!(prev_state, A::None) => {
lines.push(
"card removed — no card governs this backend; card-derived behavior \
is off (inline backend settings kept)"
.to_string(),
);
}
_ => {
if let Some(line) = applicability_prose(now) {
lines.push(line);
}
}
}
}
(
CardNotices {
last_error: error.map(str::to_string),
last_applicability: Some(now.clone()),
},
lines,
)
}
pub fn applicability_prose(a: &newt_core::model_card::CardApplicability) -> Option<String> {
use newt_core::model_card::CardApplicability as A;
match a {
A::None | A::Active { .. } => None,
A::InactiveModel {
card,
bound_model,
active_model,
} => Some(format!(
"card `{card}` is bound to {} — the session is serving {active_model}, so \
card-derived behavior (capabilities and family policy) is off (inline \
backend settings kept); rebind the card or switch back",
bound_model.as_deref().unwrap_or("(no declared model)"),
)),
A::InactiveDestination {
card,
bound_destination,
active_destination,
} => Some(format!(
"card `{card}` is bound at {} — the session is routed to {}, so \
card-derived behavior (capabilities and family policy) is off (inline \
backend settings kept)",
describe_destination(bound_destination),
describe_destination(active_destination),
)),
A::Undecided { card } => Some(format!(
"card `{card}` is configured but the serving principal is not established — \
card-derived behavior (capabilities and family policy) stays off until \
adoption decides"
)),
#[allow(unreachable_patterns)]
other => Some(format!("card state changed: {other:?}")),
}
}
fn describe_destination(d: &newt_core::BackendDestination) -> String {
d.endpoint
.clone()
.or_else(|| d.model_path.clone())
.unwrap_or_else(|| "(no destination)".into())
}
#[cfg(test)]
#[path = "lib_tests/notice_transition_tests.rs"]
mod notice_transition_tests;
fn same_intent(a: &BackendChoice, b: &BackendChoice) -> bool {
a.name == b.name
&& a.url == b.url
&& a.declared_model == b.declared_model
&& a.requested_model == b.requested_model
&& a.declared_serving == b.declared_serving
&& a.requested_serving == b.requested_serving
&& a.configured_kind == b.configured_kind
&& a.configured_api == b.configured_api
&& a.managed == b.managed
&& a.api_key == b.api_key
}
fn merge_refresh(prev: &BackendChoice, next: &mut BackendChoice) -> bool {
next.notices = prev.notices.clone();
if !same_intent(prev, next) {
return true;
}
next.kind = prev.kind;
next.kind_needs_probe = prev.kind_needs_probe;
next.api = prev.api;
next.api_needs_probe = prev.api_needs_probe;
next.adopted_serving = prev.adopted_serving;
next.active_model = prev.active_model.clone();
next.context_window = prev.context_window;
false
}
fn adoption_inputs(choice: &BackendChoice) -> (newt_core::BackendConfig, Option<String>) {
(
newt_core::BackendConfig {
name: choice.name.clone(),
endpoint: choice.url.clone(),
model: choice.declared_model.clone(),
kind: Some(choice.kind),
serving: choice
.requested_serving
.or(choice.observed_serving)
.or(choice.declared_serving),
managed: choice.managed,
..Default::default()
},
choice.requested_model.clone(),
)
}
fn apply_adoption(choice: &mut BackendChoice, adoption: &newt_core::backend_probe::Adoption) {
choice.adopted_serving = Some(adoption.serving);
choice.active_model = adoption.model.clone().filter(|m| !m.trim().is_empty());
}
fn ready_line(version: &str, model: &str, url: &str, kind: newt_core::BackendKind) -> String {
format!(
"v{version} ready — {model} @ {url} ({}){} (Ctrl-D or /exit to quit)",
kind.label(),
tenacity_indicator(newt_core::tenacity::effective_tenacity())
)
}
fn tenacity_indicator(t: newt_core::Tenacity) -> String {
if t == newt_core::Tenacity::Normal {
String::new()
} else {
format!(" · tenacity: {}", t.label())
}
}
fn should_probe_openai_surface(
kind: newt_core::BackendKind,
api_needs_probe: bool,
model: &str,
url: &str,
serving: Option<newt_core::Serving>,
) -> bool {
kind == newt_core::BackendKind::Openai
&& api_needs_probe
&& !model.is_empty()
&& (url.starts_with("https://") || serving == Some(newt_core::Serving::Instance))
}
fn resolve_embeddings_target(
cfg: &newt_core::SemanticConfig,
inf_url: &str,
inf_kind: newt_core::BackendKind,
inf_key: Option<&str>,
) -> (String, newt_core::BackendKind, Option<String>) {
match cfg.embeddings_endpoint.clone() {
Some(url) => (
url,
cfg.embeddings_api.unwrap_or(newt_core::BackendKind::Ollama),
None,
),
None => (
inf_url.to_string(),
cfg.embeddings_api.unwrap_or(inf_kind),
inf_key.map(str::to_string),
),
}
}
pub(crate) fn active_backend_name(resolved: &newt_core::ResolvedConfig) -> Option<String> {
if let Some(name) = std::env::var("NEWT_PROVIDER")
.ok()
.filter(|s| !s.is_empty())
{
if resolved.backends.iter().any(|b| b.name == name) {
return Some(name);
}
}
let choice = resolve_backend_choice(resolved).ok()?;
resolved
.backends
.iter()
.find(|b| b.endpoint == choice.url && (b.kind.is_none() || b.kind == Some(choice.kind)))
.map(|b| b.name.clone())
}
fn adopt_backend_choice(choice: &mut BackendChoice, prewarm: Option<Prewarm>) -> Vec<String> {
use newt_core::backend_probe;
if choice.kind == newt_core::BackendKind::Embedded && !choice.kind_needs_probe {
return Vec::new();
}
let lines = Vec::new();
let prewarmed = prewarm
.filter(|pw| prewarm_applies(&choice.url, &pw.url))
.and_then(|pw| {
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(pw.handle))
.ok()
.flatten()
});
if let Some(probe) = prewarmed {
let detected = if choice.kind_needs_probe {
choice.kind = probe.kind;
choice.kind_needs_probe = false;
if choice.route_serving().is_none() {
choice.adopted_serving = Some(probe.serving);
}
Some(probe.kind)
} else {
None
};
return finish_adoption(choice, lines, probe.models, probe.warm, detected);
}
let secs = probe_timeout_secs(&choice.url);
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(secs))
.build()
{
Ok(c) => c,
Err(_) => return Vec::new(),
};
let fetched = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
if choice.kind_needs_probe {
match backend_probe::detect_endpoint(
&client,
&choice.url,
choice.api_key.as_deref(),
)
.await
{
Ok(probe) => {
choice.kind = probe.kind;
choice.kind_needs_probe = false;
if choice.route_serving().is_none() {
choice.adopted_serving = Some(probe.serving);
}
Ok((probe.models, probe.warm, Some(probe.kind)))
}
Err(e) => Err(e),
}
} else {
let api = backend_probe::api_for(choice.kind);
match api
.list_models(&client, &choice.url, choice.api_key.as_deref())
.await
{
Ok(models) => {
let warm = api
.warm_models(&client, &choice.url, choice.api_key.as_deref())
.await
.unwrap_or_default();
Ok((models, warm, None))
}
Err(e) => Err(e),
}
}
})
});
match fetched {
Ok((models, warm, detected_kind)) => {
finish_adoption(choice, lines, models, warm, detected_kind)
}
Err(e) => offline_adoption(choice, lines, e),
}
}
fn resolve_modal_choice(picked: Option<(String, Vec<String>)>) -> Option<String> {
picked.and_then(|(m, served)| served.contains(&m).then_some(m))
}
#[cfg_attr(not(feature = "rich-tui"), allow(unused_variables))]
fn pick_ambiguous_default(choice: &BackendChoice) -> Option<(String, Vec<String>)> {
#[cfg(feature = "rich-tui")]
{
std::io::IsTerminal::is_terminal(&std::io::stdout())
.then(|| models_panel::choose(choice, None).ok().flatten())
.flatten()
}
#[cfg(not(feature = "rich-tui"))]
{
None
}
}
fn finish_adoption(
choice: &mut BackendChoice,
lines: Vec<String>,
models: Vec<String>,
warm: Vec<String>,
detected_kind: Option<newt_core::BackendKind>,
) -> Vec<String> {
finish_adoption_with_picker(
choice,
lines,
models,
warm,
detected_kind,
pick_ambiguous_default,
)
}
fn finish_adoption_with_picker(
choice: &mut BackendChoice,
mut lines: Vec<String>,
models: Vec<String>,
warm: Vec<String>,
detected_kind: Option<newt_core::BackendKind>,
picker: impl FnOnce(&BackendChoice) -> Option<(String, Vec<String>)>,
) -> Vec<String> {
use newt_core::backend_probe::{self, Served};
let secs = probe_timeout_secs(&choice.url);
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(secs))
.build()
{
Ok(c) => c,
Err(_) => return lines,
};
{
{
if let Some(kind) = detected_kind {
lines.push(format!(
"detected {} at {} — {} model(s)",
kind.label(),
choice.url,
models.len()
));
}
let (synth, requested) = adoption_inputs(choice);
let warm_at_probe = warm.clone();
let declared_name = synth.effective_model().map(str::to_string);
let mut adoption =
backend_probe::adopt(&synth, &Served { models, warm }, requested.as_deref());
if adoption.requested_unavailable {
lines.push(match adoption.model.as_deref() {
Some(m) => format!(
"requested model isn't on {} — falling back to {}; was it a typo, \
or removed from the endpoint? /models to list",
choice.url, m
),
None => format!(
"requested model isn't on {} — falling back (was it a typo, or \
removed from the endpoint?); /models to list",
choice.url
),
});
}
let backend_default =
requested.is_none() && (declared_name.is_none() || adoption.declared_unavailable);
if !adoption.ambiguous_warm.is_empty() {
let last_used = newt_core::Config::user_config_path()
.map(|p| p.with_file_name("usage.jsonl"))
.and_then(|log| {
newt_core::metrics::last_eligible_model_for_endpoint(
&log,
&choice.url,
&adoption.ambiguous_warm,
)
});
match last_used {
Some(m) => {
lines.push(format!(
"backend default → {m} (last used on this backend; also loaded: \
{})",
adoption
.ambiguous_warm
.iter()
.filter(|w| *w != &m)
.cloned()
.collect::<Vec<_>>()
.join(", ")
));
adoption.model = Some(m);
adoption.ambiguous_warm.clear();
}
None => {
let picked = picker(choice);
match resolve_modal_choice(picked) {
Some(m) => {
lines.push(format!("backend default → {m} (picked)"));
adoption.model = Some(m);
adoption.ambiguous_warm.clear();
}
None => lines.push(format!(
"{} has {} models loaded ({}) and none is configured — \
pick one with /model <name>; /models to list",
choice.url,
adoption.ambiguous_warm.len(),
adoption.ambiguous_warm.join(", ")
)),
}
}
}
} else if backend_default {
if let Some(m) = adoption.model.as_deref() {
let loaded = warm_at_probe.iter().any(|w| w == m);
lines.push(format!(
"backend default → {m}{}",
if loaded { " (loaded)" } else { "" }
));
}
if adoption.declared_unavailable {
if let Some(stale) = &declared_name {
lines.push(format!(
"configured model \"{stale}\" isn't served by {} (renamed or \
removed?)",
choice.url
));
}
}
}
if adoption.model.is_none() && adoption.ambiguous_warm.is_empty() {
lines.push(format!(
"{} listed no models — pull one (or start the server with a model), \
then /models",
choice.url
));
} else if adoption.requested_ignored {
lines.push(format!(
"model is fixed by this {} instance: {} — restart the server \
with another model, or /backends to switch endpoints",
choice.kind.label(),
adoption.model.as_deref().unwrap_or_default()
));
}
apply_adoption(choice, &adoption);
let window_model = choice.active_model.clone().unwrap_or_default();
choice.context_window = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
backend_probe::api_for(choice.kind)
.context_window(
&client,
&choice.url,
&window_model,
choice.api_key.as_deref(),
)
.await
})
});
let mut api_was_probed = false;
if should_probe_openai_surface(
choice.kind,
choice.api_needs_probe,
&window_model,
&choice.url,
choice.route_serving(),
) {
match tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
backend_probe::detect_openai_api(
&client,
&choice.url,
&window_model,
choice.api_key.as_deref(),
)
.await
})
}) {
Ok(api) => {
choice.api = api;
choice.api_needs_probe = false;
api_was_probed = true;
lines.push(format!(
"detected api={} for {} @ {}",
api.label(),
choice.display_model(),
choice.url
));
}
Err(e) => lines.push(format!(
"api probe failed ({e:#}) — using chat_completions until it answers"
)),
}
}
if !choice.name.is_empty() && (detected_kind.is_some() || api_was_probed) {
let serving = match choice.route_serving() {
Some(newt_core::Serving::Instance) => newt_core::ProbedServing::Instance {
model: choice.active_model.clone(),
},
Some(newt_core::Serving::Multiplexer) => newt_core::ProbedServing::Multiplexer,
None => newt_core::ProbedServing::Unknown,
};
let observation = newt_core::ProbeObservation {
name: choice.name.clone(),
endpoint: choice.url.clone(),
kind: Some(choice.kind),
api: (choice.kind == newt_core::BackendKind::Openai).then_some(choice.api),
serving,
};
match newt_core::persist_probe_observation(&observation) {
Ok(newt_core::ProbeWriteback::Written(path)) => lines.push(format!(
"wrote probed backend → {} (delete to reset)",
path.display()
)),
Ok(newt_core::ProbeWriteback::SkippedOperatorOwned(path)) => {
lines.push(format!(
"probe results not persisted — {} is operator-owned \
(delete it, or claim/edit it, to let probes write)",
path.display()
));
}
Ok(newt_core::ProbeWriteback::NotWritten) => {}
Err(e) => lines.push(format!("could not write backend drop-in: {e}")),
}
}
lines.extend(choice.card_notice_lines());
}
}
lines
}
fn offline_adoption(
choice: &mut BackendChoice,
mut lines: Vec<String>,
e: anyhow::Error,
) -> Vec<String> {
match choice.active_model.as_deref() {
None => lines.push(format!(
"{} is unreachable ({e:#}) and no model is configured — check the \
endpoint, then /backends",
choice.url
)),
Some(model) => lines.push(format!(
"{} is unreachable ({e:#}) — using configured model {model} until it answers",
choice.url
)),
}
lines
}
pub(crate) fn backends_list_items(
cfg: &newt_core::Config,
active: Option<&str>,
) -> Vec<(String, bool)> {
cfg.backends
.iter()
.map(|b| {
let label = format!(
"{} · {} · {} @ {}",
b.name,
b.kind_label(),
b.effective_model().unwrap_or("(server decides)"),
b.endpoint
);
(label, active == Some(b.name.as_str()))
})
.collect()
}
pub(crate) fn resolve_backend_choice(
resolved: &newt_core::ResolvedConfig,
) -> Result<BackendChoice, String> {
let session_model = || {
std::env::var("NEWT_DGX_MODEL")
.ok()
.filter(|s| !s.is_empty())
};
let from_backend = |rb: newt_core::ResolvedBackend<'_>| -> Result<BackendChoice, String> {
let b = rb.backend;
let receipt = rb.receipt;
if b.endpoint.is_empty() || b.kind == Some(newt_core::BackendKind::Embedded) {
return Err(format!(
"backend `{}` is an embedded (model_path) backend — chat drives HTTP \
backends only; select an HTTP backend (/backends), or serve the \
artifact behind an HTTP endpoint",
b.name
));
}
let seed = receipt.binding.clone();
let pinned = newt_core::Config::pinned_config_path();
let (caps, card_error) =
match newt_core::model_card::ResolvedCapabilities::resolve(b, &seed, pinned.as_deref())
{
Ok(caps) => (caps, None),
Err(e) => (
newt_core::model_card::ResolvedCapabilities::resolve(
b,
&newt_core::model_card::CardBindingSeed {
card: None,
..seed.clone()
},
None,
)
.expect("card-less resolution is infallible"),
Some(e),
),
};
let (observed_serving, _observed_model) = receipt
.observation
.as_ref()
.map(|o| o.serving_axis())
.unwrap_or((None, None));
let request = receipt.request.as_ref();
let declared_serving = receipt.declaration.serving.or_else(|| {
receipt
.declaration
.destination
.model_path
.as_deref()
.is_some_and(|p| !p.is_empty())
.then_some(newt_core::Serving::Instance)
});
Ok(BackendChoice {
name: b.name.clone(),
url: b.endpoint.clone(),
declared_model: receipt.declaration.model.clone(),
requested_model: session_model()
.or_else(|| receipt.request.as_ref().and_then(|r| r.model.clone())),
declared_serving,
requested_serving: request.and_then(|r| r.serving),
configured_kind: request.and_then(|r| r.kind).or(receipt.declaration.kind),
configured_api: request.and_then(|r| r.api).or(receipt.declaration.api),
managed: receipt.declaration.managed,
api_key: b.resolve_api_key(),
model_path: b.model_path.clone(),
active_model: session_model().or_else(|| b.effective_model().map(str::to_string)),
observed_serving,
adopted_serving: None,
kind: b.kind.unwrap_or(newt_core::BackendKind::Ollama),
kind_needs_probe: b.needs_kind_probe(),
api: b.api.unwrap_or_default(),
api_needs_probe: b.api.is_none(),
context_window: None,
capabilities: caps,
card_resolution_error: card_error,
notices: CardNotices::default(),
})
};
let explicit_selector = std::env::var("NEWT_PROVIDER")
.ok()
.filter(|s| !s.is_empty())
.is_some()
|| resolved
.default_backend
.as_deref()
.is_some_and(|n| !n.is_empty());
if explicit_selector {
use newt_core::config::{SelectedBackend, SelectionOutcome};
match resolved.select_backend() {
SelectionOutcome::Selected(SelectedBackend::Configured(_)) => {
let rb = resolved
.selected_backend()
.expect("the shared index selector just picked a configured backend");
return from_backend(rb);
}
SelectionOutcome::Selected(SelectedBackend::Provider(p)) => {
return Err(format!(
"the backend selection resolves to provider `{}` — chat drives \
[[backends]] only; point $NEWT_PROVIDER/default_backend at a \
backend (or unset them)",
p.name
));
}
SelectionOutcome::UnknownNamed(name) => {
return Err(format!(
"$NEWT_PROVIDER/default_backend names `{name}`, which matches \
nothing configured — fix the selector (chat will not silently \
run another backend)"
));
}
SelectionOutcome::UnroutableNamed(name) => {
return Err(format!(
"$NEWT_PROVIDER/default_backend names `{name}`, which has neither \
an endpoint nor a model_path — give it a destination (chat will \
not silently run another backend)"
));
}
SelectionOutcome::Unset => {}
}
}
{
let base = std::env::var("OPENAI_BASE_URL").ok();
let key = std::env::var("OPENAI_API_KEY").ok();
let model = std::env::var("OPENAI_MODEL").ok();
if let Some(choice) = codex_env_backend(
base.as_deref(),
key.as_deref(),
model.as_deref(),
session_model(),
!resolved.backends.is_empty(),
) {
let detected = [
base.as_ref().map(|_| "OPENAI_BASE_URL"),
key.as_ref().map(|_| "OPENAI_API_KEY"),
model.as_ref().map(|_| "OPENAI_MODEL"),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(", ");
if codex_env_allowed(&detected) {
return Ok(choice);
}
}
}
let env_url = std::env::var("NEWT_DGX_OLLAMA_URL").ok().or_else(|| {
std::env::var("NEWT_DGX_HOST").ok().map(|h| {
let scheme = std::env::var("NEWT_DGX_SCHEME").unwrap_or_else(|_| "http".into());
let port = std::env::var("NEWT_DGX_OLLAMA_PORT").unwrap_or_else(|_| "11434".into());
format!("{scheme}://{h}:{port}")
})
});
if let Some(url) = env_url {
let requested = session_model();
return Ok(BackendChoice {
requested_model: requested.clone(),
..BackendChoice::synthesized(
"",
url,
newt_core::BackendKind::Ollama,
requested.or_else(|| Some("llama3.1:8b".into())),
)
});
}
if let Some(force) = std::env::var("NEWT_BACKEND").ok().filter(|s| !s.is_empty()) {
let want = if force.eq_ignore_ascii_case("openai") {
newt_core::BackendKind::Openai
} else {
newt_core::BackendKind::Ollama
};
if let Some(rb) = resolved.backends().find(|rb| rb.backend.kind == Some(want)) {
return from_backend(rb);
}
}
{
use newt_core::config::{SelectedBackend, SelectionOutcome};
match resolved.select_backend() {
SelectionOutcome::Selected(SelectedBackend::Configured(_)) => {
let rb = resolved
.selected_backend()
.expect("the shared index selector just picked a configured backend");
return from_backend(rb);
}
SelectionOutcome::Selected(SelectedBackend::Provider(p)) => {
return Err(format!(
"the backend selection resolves to provider `{}` — chat drives \
[[backends]] only; configure a backend (or run the worker)",
p.name
));
}
SelectionOutcome::UnknownNamed(name) => {
return Err(format!(
"$NEWT_PROVIDER/default_backend names `{name}`, which matches \
nothing configured — fix the selector"
));
}
SelectionOutcome::UnroutableNamed(name) => {
return Err(format!(
"$NEWT_PROVIDER/default_backend names `{name}`, which has neither \
an endpoint nor a model_path — give it a destination"
));
}
SelectionOutcome::Unset => {}
}
}
if let Some((url, model)) = resolved.dgx.as_ref().and_then(|d| {
d.nodes.first().and_then(|n| n.ollama.clone()).map(|url| {
let model = session_model()
.or_else(|| d.active_model.clone())
.unwrap_or_else(|| "llama3.1:8b".into());
(url, model)
})
}) {
let requested = session_model();
return Ok(BackendChoice {
requested_model: requested,
..BackendChoice::synthesized("", url, newt_core::BackendKind::Ollama, Some(model))
});
}
let requested = session_model();
Ok(BackendChoice {
requested_model: requested.clone(),
..BackendChoice::synthesized(
"",
"http://localhost:11434".into(),
newt_core::BackendKind::Ollama,
requested.or_else(|| Some("llama3.1:8b".into())),
)
})
}
pub(crate) fn resolve_runtime_or_default() -> newt_core::ResolvedConfig {
crate::migration_notices::read(|report| newt_core::Config::resolve_runtime_unpublished(report))
.unwrap_or_else(|e| {
tracing::warn!(error = %e, "config resolution failed — running on built-in defaults");
newt_core::ResolvedConfig::unrequested(newt_core::Config::default())
})
}
pub fn apply_openai_api_env(api: newt_core::OpenAiApi) {
newt_core::process_env::set_or_remove(
"NEWT_OPENAI_API",
match api {
newt_core::OpenAiApi::Responses => Some("responses"),
newt_core::OpenAiApi::ChatCompletions => None,
},
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn refresh_backend(
resolved: &newt_core::ResolvedConfig,
choice: &mut BackendChoice,
inf_url: &mut String,
inf_model: &mut String,
inf_kind: &mut newt_core::BackendKind,
inf_key: &mut Option<String>,
inf_context_window: &mut Option<u32>,
color: bool,
verbose: bool,
) -> bool {
let prev_url = inf_url.clone();
let mut next = match resolve_backend_choice(resolved) {
Ok(next) => next,
Err(refusal) => {
print_newt(&refusal, color, verbose);
return false;
}
};
resolved.publish_runtime_settings();
let _intent_changed = merge_refresh(choice, &mut next);
*choice = next;
if choice.url != prev_url || choice.active_model.clone().unwrap_or_default() != *inf_model {
for line in adopt_backend_choice(choice, None) {
print_newt(&line, color, verbose);
}
}
for line in choice.card_notice_lines() {
print_newt(&line, color, verbose);
}
*inf_url = choice.url.clone();
*inf_model = choice.active_model.clone().unwrap_or_default();
*inf_kind = choice.kind;
*inf_key = choice.api_key.clone();
*inf_context_window = choice.context_window;
apply_openai_api_env(choice.api);
newt_core::initiative::set_active_model_family(
choice
.capabilities
.family_for_route(&choice.route_destination(), choice.principal())
.map(str::to_string),
);
*inf_url != prev_url
}
pub(crate) fn tab_label(store: &newt_core::ConversationStore, conversation_id: &str) -> String {
store
.title(conversation_id)
.ok()
.flatten()
.filter(|title| !title.trim().is_empty())
.unwrap_or_else(|| format!("#{}", short_conversation_id(conversation_id)))
}
pub(crate) fn rename_conversation(
store: &newt_core::ConversationStore,
conversation_id: &str,
title: &str,
persona: Option<&str>,
) -> anyhow::Result<()> {
let title = title.trim();
if title.is_empty() {
anyhow::bail!("a title cannot be empty");
}
match store.exists(conversation_id)? {
true => store.rename(conversation_id, title),
false => store.create_with_id(conversation_id, title, persona),
}
}
pub(crate) fn persist_preference_actions(
store: Option<&newt_core::ConversationStore>,
conversation_id: &str,
pending: &mut newt_core::PreferenceActions,
base_provider: &mut Option<String>,
base_model: &mut Option<String>,
) -> Result<(), String> {
let fresh = newt_core::runtime::drain_preference_actions();
if let Some(provider) = &fresh.backend {
base_provider.clone_from(provider);
}
if let Some(model) = &fresh.model {
base_model.clone_from(model);
}
pending.merge(fresh);
if pending.is_empty() {
return Ok(());
}
let Some(store) = store else {
*pending = newt_core::PreferenceActions::default();
return Ok(());
};
let stored = match store.preference_pin(conversation_id) {
Ok(None) => return Ok(()),
Ok(Some(pin)) => pin,
Err(e) => {
*pending = newt_core::PreferenceActions::default();
return Err(format!(
"could not read the conversation preference pin ({e}) — \
this change applies to the session but is not pinned"
));
}
};
let merged = stored.merged(pending);
*pending = newt_core::PreferenceActions::default();
if merged == stored {
return Ok(());
}
store
.update_preference_pin(conversation_id, &merged)
.map_err(|e| format!("could not persist the conversation preference pin: {e}"))
}
#[cfg(test)]
#[path = "lib_tests/resumed_preference_tests.rs"]
mod resumed_preference_tests;
#[allow(dead_code)]
fn build_system_prompt(workspace: &str, plan_path: &str) -> String {
build_system_prompt_with_soul(workspace, None, plan_path)
}
pub(crate) fn seat_persona_dials(persona: Option<&Persona>) {
let profile = persona.map(|p| &p.profile);
newt_core::cognition::set_persona_cognition(profile.and_then(|p| p.cognition));
newt_core::initiative::set_persona_initiative(profile.and_then(|p| p.initiative));
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Persona {
name: String,
prompt: String,
path: std::path::PathBuf,
profile: newt_core::RoleProfile,
}
impl Persona {
fn description(&self) -> String {
self.prompt
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.unwrap_or("")
.trim_start_matches('#')
.trim()
.chars()
.take(96)
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PersonaSummary {
name: String,
description: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PersonaStore {
dir: std::path::PathBuf,
}
#[cfg(feature = "rich-tui")]
#[derive(Debug)]
enum PersonaSaveError {
Exists,
InvalidName(String),
Io(String),
}
impl PersonaStore {
const DEFAULT_NAME: &'static str = "coder";
fn default_dir() -> std::path::PathBuf {
newt_core::Config::user_config_path()
.map(|p| p.with_file_name("personas"))
.unwrap_or_else(|| std::path::PathBuf::from("personas"))
}
fn new(dir: impl Into<std::path::PathBuf>) -> Self {
Self { dir: dir.into() }
}
fn default() -> Self {
Self::new(Self::default_dir())
}
#[cfg(feature = "rich-tui")]
fn save(
&self,
name: &str,
content: &str,
overwrite: bool,
) -> Result<std::path::PathBuf, PersonaSaveError> {
self.save_with(name, content, overwrite, |p, c| std::fs::write(p, c))
}
#[cfg(feature = "rich-tui")]
fn save_with<W>(
&self,
name: &str,
content: &str,
overwrite: bool,
write_bytes: W,
) -> Result<std::path::PathBuf, PersonaSaveError>
where
W: Fn(&std::path::Path, &str) -> std::io::Result<()>,
{
let name = normalize_persona_name(name)
.map_err(|e| PersonaSaveError::InvalidName(e.to_string()))?;
let path = self.dir.join(format!("{name}.md"));
if !overwrite && path.exists() {
return Err(PersonaSaveError::Exists);
}
std::fs::create_dir_all(&self.dir).map_err(|e| PersonaSaveError::Io(e.to_string()))?;
let tmp = self
.dir
.join(format!(".{name}.md.tmp.{}", std::process::id()));
if let Err(e) = write_bytes(&tmp, content) {
let _ = std::fs::remove_file(&tmp);
return Err(PersonaSaveError::Io(e.to_string()));
}
match std::fs::rename(&tmp, &path) {
Ok(()) => Ok(path),
Err(e) => {
let _ = std::fs::remove_file(&tmp);
Err(PersonaSaveError::Io(e.to_string()))
}
}
}
fn load(&self, name: &str) -> anyhow::Result<Persona> {
crate::migration_notices::read(|report| {
self.ensure_defaults()?;
let name = normalize_persona_name(name)?;
let path = self.dir.join(format!("{name}.md"));
let raw = match newt_core::psyche_import::read_persona_file(&path, report) {
Ok(raw) => raw,
Err(_) => anyhow::bail!("unknown persona `{name}`\n{}", self.list_message()?),
};
let profile = newt_core::RoleProfile::parse(&raw)
.map_err(|e| anyhow::anyhow!("persona `{name}`: {e}"))?;
if profile.prompt.is_empty() {
anyhow::bail!("persona `{name}` is empty: {}", path.display());
}
Ok(Persona {
name,
prompt: profile.prompt.clone(),
path,
profile,
})
})
}
fn list(&self) -> anyhow::Result<Vec<PersonaSummary>> {
crate::migration_notices::read(|report| {
self.ensure_defaults()?;
let mut personas = Vec::new();
for entry in std::fs::read_dir(&self.dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("md") {
continue;
}
let Some(name) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
let raw =
newt_core::psyche_import::read_persona_file(&path, report).unwrap_or_default();
if raw.trim().is_empty() {
continue;
}
let Ok(profile) = newt_core::RoleProfile::parse(&raw) else {
continue;
};
let persona = Persona {
name: name.to_string(),
prompt: profile.prompt.clone(),
path: path.clone(),
profile,
};
let description = persona.description();
personas.push(PersonaSummary {
name: persona.name,
description,
});
}
personas.sort_by(|a, b| a.name.cmp(&b.name));
Ok(personas)
})
}
fn list_message(&self) -> anyhow::Result<String> {
let personas = self.list()?;
let mut out = format!("Available personas in {}:", self.dir.display());
if personas.is_empty() {
out.push_str("\n (none)");
} else {
for p in personas {
out.push_str(&format!("\n {} - {}", p.name, p.description));
}
}
Ok(out)
}
const DEFAULT_PERSONAS: &'static [(&'static str, &'static str)] = &[
(Self::DEFAULT_NAME, newt_core::DEFAULT_SOUL),
("coach", COACH_PERSONA),
("personal-assistant", PERSONAL_ASSISTANT_PERSONA),
("steady", include_str!("../../personas/steady.md")),
("direct", include_str!("../../personas/direct.md")),
("sociable", include_str!("../../personas/sociable.md")),
];
fn ensure_defaults(&self) -> anyhow::Result<()> {
std::fs::create_dir_all(&self.dir)?;
for &(name, body) in Self::DEFAULT_PERSONAS {
let path = self.dir.join(format!("{name}.md"));
if !path.exists() {
std::fs::write(&path, body)?;
}
}
Ok(())
}
}
const COACH_PERSONA: &str = include_str!("../../personas/coach.md");
const PERSONAL_ASSISTANT_PERSONA: &str = include_str!("../../personas/personal-assistant.md");
fn missing_bound_skills(skills: &[String], search_dirs: &[std::path::PathBuf]) -> Vec<String> {
if skills.is_empty() {
return Vec::new();
}
let available: std::collections::HashSet<String> = newt_skills::discover_paths(search_dirs)
.into_iter()
.map(|s| s.name)
.collect();
skills
.iter()
.filter(|name| !available.contains(*name))
.cloned()
.collect()
}
fn warn_on_missing_bound_skills(
persona: Option<&Persona>,
search_dirs: &[std::path::PathBuf],
color: bool,
verbose: bool,
) {
let Some(persona) = persona else { return };
let Some(skills) = &persona.profile.skills else {
return;
};
let missing = missing_bound_skills(skills, search_dirs);
if !missing.is_empty() {
print_newt(
&format!(
"warning: persona `{}` declares skill(s) not found in any skill search dir: {}",
persona.name,
missing.join(", ")
),
color,
verbose,
);
}
}
const GILA_SKILL: &str = include_str!("../assets/bundled-skills/gila-personal-assistant/SKILL.md");
fn ensure_default_skills(cfg: &newt_core::Config) -> anyhow::Result<()> {
match cfg.skill_install_dir() {
Some(dir) => seed_gila_skill(&dir),
None => Ok(()),
}
}
fn seed_gila_skill(skills_root: &std::path::Path) -> anyhow::Result<()> {
let skill_dir = skills_root.join("gila-personal-assistant");
let path = skill_dir.join("SKILL.md");
if !path.exists() {
std::fs::create_dir_all(&skill_dir)?;
std::fs::write(&path, GILA_SKILL)?;
}
Ok(())
}
fn normalize_persona_name(name: &str) -> anyhow::Result<String> {
let name = name.trim().to_ascii_lowercase();
if name.is_empty()
|| !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
anyhow::bail!("persona names may only contain letters, numbers, '-' and '_'");
}
Ok(name)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum PersonaCommand {
List,
Show,
Clear,
Set {
name: String,
keep_context: bool,
},
}
#[cfg(test)]
impl PersonaCommand {
fn set(name: impl Into<String>) -> Self {
Self::Set {
name: name.into(),
keep_context: false,
}
}
}
fn parse_persona_command(input: &str) -> anyhow::Result<PersonaCommand> {
let body = input.trim().trim_start_matches('/').trim();
let mut parts = body.split_whitespace();
match parts.next() {
Some("persona") => {}
_ => anyhow::bail!("not a persona command"),
}
let mut keep_context = false;
let positional: Vec<&str> = parts
.filter(|tok| {
if *tok == "--keep-context" {
keep_context = true;
false
} else {
true
}
})
.collect();
let mut positional = positional.into_iter();
match positional.next() {
None | Some("show") => Ok(PersonaCommand::Show),
Some("list") => Ok(PersonaCommand::List),
Some("clear" | "off") => Ok(PersonaCommand::Clear),
Some("default") => Ok(PersonaCommand::Set {
name: PersonaStore::DEFAULT_NAME.into(),
keep_context,
}),
Some(verb @ ("set" | "switch")) => match positional.next() {
Some(name) => Ok(PersonaCommand::Set {
name: name.to_string(),
keep_context,
}),
None => anyhow::bail!("usage: /persona {verb} <name> [--keep-context]"),
},
Some(name) => Ok(PersonaCommand::Set {
name: name.to_string(),
keep_context,
}),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ConversationCommand {
List,
Show(String),
Restore(String),
Rename { id: String, title: String },
Delete(String),
}
pub(crate) fn conversation_command_target(input: &str) -> Option<String> {
match parse_conversation_command(input).ok()? {
ConversationCommand::Restore(id) => Some(id),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConversationDoor {
Resume,
Retired,
}
impl ConversationCommand {
pub(crate) fn mutates(&self) -> bool {
!matches!(self, Self::List | Self::Show(_))
}
}
fn parse_conversation_command_at(
input: &str,
) -> anyhow::Result<(ConversationDoor, ConversationCommand)> {
let body = input.trim().trim_start_matches('/').trim();
let mut parts = body.split_whitespace();
let door = match parts.next() {
Some("conversation") => ConversationDoor::Retired,
Some("resume") => ConversationDoor::Resume,
_ => anyhow::bail!("not a conversation command"),
};
parse_conversation_rest(door, parts)
}
fn parse_conversation_command(input: &str) -> anyhow::Result<ConversationCommand> {
Ok(parse_conversation_command_at(input)?.1)
}
const CONVERSATION_SUBCOMMANDS: &[&str] = &["list", "show", "restore", "rename", "delete", "rm"];
pub(crate) fn resume_conversation_subcommand(body: &str) -> bool {
let Some(rest) = body.strip_prefix("resume") else {
return false;
};
let rest = match rest.chars().next() {
None => return false,
Some(c) if c.is_whitespace() => rest.trim_start(),
Some(_) => return false,
};
rest.split_whitespace()
.next()
.is_some_and(|word| CONVERSATION_SUBCOMMANDS.contains(&word))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ConversationOpPlan {
Run,
Confirm { prompt: String, id: String },
Redirect(String),
}
pub(crate) fn conversation_op_plan(input: &str) -> anyhow::Result<ConversationOpPlan> {
let (door, command) = parse_conversation_command_at(input)?;
if door == ConversationDoor::Retired && command.mutates() {
let replacement = match &command {
ConversationCommand::Restore(id) => format!("/resume restore {id}"),
ConversationCommand::Rename { id, title } => format!("/resume rename {id} {title}"),
ConversationCommand::Delete(id) => format!("/resume delete {id}"),
ConversationCommand::List | ConversationCommand::Show(_) => unreachable!(
"List and Show do not mutate; `mutates()` is the one predicate that says so"
),
};
return Ok(ConversationOpPlan::Redirect(format!(
"/conversation is retired — use `{replacement}` (nothing changed)"
)));
}
if let ConversationCommand::Delete(id) = &command {
return Ok(ConversationOpPlan::Confirm {
prompt: format!(
"delete conversation `{id}` and every turn in it? this cannot be undone"
),
id: id.clone(),
});
}
Ok(ConversationOpPlan::Run)
}
pub(crate) fn confirm_conversation_delete(ask: SlashAsk<'_>, prompt: &str) -> bool {
let definition = newt_core::interaction_form::confirm(
prompt.to_string(),
"this removes the conversation and its turns",
"delete it",
"keep it",
);
let interaction =
newt_core::interaction_surface::SurfaceInteraction::blocking(definition.clone());
match ask(&interaction) {
newt_core::HumanQuestionOutcome::Answer(answer) => {
newt_core::interaction_form::resolve(&definition, answer.trim())
.is_some_and(|id| id.as_str() == newt_core::interaction_form::YES)
}
_ => false,
}
}
fn parse_conversation_rest<'a>(
door: ConversationDoor,
mut parts: impl Iterator<Item = &'a str>,
) -> anyhow::Result<(ConversationDoor, ConversationCommand)> {
let verb = match door {
ConversationDoor::Resume => "resume",
ConversationDoor::Retired => "conversation",
};
let command = match parts.next() {
None | Some("list") => ConversationCommand::List,
Some("show") => match parts.next() {
Some(id) => ConversationCommand::Show(id.to_string()),
None => anyhow::bail!("usage: /{verb} show <id>"),
},
Some("restore") => match parts.next() {
Some(id) => ConversationCommand::Restore(id.to_string()),
None => anyhow::bail!("usage: /{verb} restore <id>"),
},
Some("rename") => {
let Some(id) = parts.next() else {
anyhow::bail!("usage: /{verb} rename <id> <title>");
};
let title = parts.collect::<Vec<_>>().join(" ");
if title.trim().is_empty() {
anyhow::bail!("usage: /{verb} rename <id> <title>");
}
ConversationCommand::Rename {
id: id.to_string(),
title,
}
}
Some("delete" | "rm") => match parts.next() {
Some(id) => ConversationCommand::Delete(id.to_string()),
None => anyhow::bail!("usage: /{verb} delete <id>"),
},
Some(other) => anyhow::bail!("unknown conversation command `{other}`"),
};
Ok((door, command))
}
fn skills_index_for_prompt(skills_dirs: &[std::path::PathBuf]) -> Option<String> {
newt_skills::index_block(&newt_skills::discover_paths(skills_dirs))
}
fn build_system_prompt_with_soul(workspace: &str, soul: Option<&str>, plan_path: &str) -> String {
build_system_prompt_with_persona(workspace, soul, None, plan_path)
}
fn synthetic_altitude_persona(altitude: newt_core::Altitude) -> Persona {
let name = match altitude {
newt_core::Altitude::Coach => "coach",
newt_core::Altitude::Doer => "doer",
};
Persona {
name: name.to_string(),
prompt: String::new(),
path: std::path::PathBuf::new(),
profile: newt_core::RoleProfile {
altitude: Some(altitude),
..Default::default()
},
}
}
const NARRATION_INTENT_LINE: &str = "**Say what you are doing.** Before each tool call, state in \
one short sentence what you are about to do and why; then make the call in the same reply.";
pub(crate) fn narration_intent_line(enabled: bool) -> Option<&'static str> {
enabled.then_some(NARRATION_INTENT_LINE)
}
fn build_system_prompt_with_persona(
workspace: &str,
soul: Option<&str>,
persona: Option<&Persona>,
plan_path: &str,
) -> String {
let effective_altitude = persona.and_then(|p| p.profile.altitude).unwrap_or_default();
let identity = match effective_altitude {
newt_core::Altitude::Coach => newt_core::COACH_SOUL,
newt_core::Altitude::Doer => soul.unwrap_or(newt_core::DEFAULT_SOUL),
};
let mut ctx = format!(
"{identity}\n\nWorkspace: {workspace}\n\
This is the current runtime root. Relative tool paths resolve here; \
run_command starts here unless its cwd or command selects another directory. \
Other requested paths remain governed by the session's permissions. \
Historical notes do not redefine this root.\n"
);
if let Some(persona) = persona {
ctx.push_str(&format!(
"\nActive persona: {}\n{}\n",
persona.name, persona.prompt
));
ctx.push_str(
"\nPersona guidance does not override the operator's explicit task. \
Treat advice-only wording and preferred tools as defaults; use the tools \
needed for the requested work. OCAP permissions, explicit postures, and \
delegation limits still govern every action.\n",
);
}
let cfg = crate::migration_notices::read(|report| newt_core::Config::resolve(report));
ctx.push_str(&format!(
"\n**Plan before coding.** For any task requiring more than one file \
change, write a plan to `{plan_path}` first (create it if it does not \
exist). List the concrete steps and check them off as you complete \
each one. This plan file is unique to this session — read it when \
resuming so you can pick up exactly where you left off without \
re-reading the whole codebase.\n"
));
ctx.push_str(newt_core::agentic::REFACTOR_PUBLISH_GUIDANCE);
ctx.push('\n');
let intent = cfg
.as_ref()
.ok()
.and_then(|c| c.tui.as_ref())
.is_some_and(|t| t.narration_intent_line);
if let Some(line) = narration_intent_line(intent) {
ctx.push_str(&format!("\n{line}\n"));
}
let skills_dirs = cfg.map(|c| c.skill_search_dirs()).unwrap_or_default();
if let Some(index) = skills_index_for_prompt(&skills_dirs) {
ctx.push('\n');
ctx.push_str(&index);
}
if let Ok(mut entries) = std::fs::read_dir(workspace) {
let mut names: Vec<String> = entries
.by_ref()
.flatten()
.filter_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
if name.starts_with('.') {
None
} else {
Some(name)
}
})
.collect();
names.sort();
ctx.push_str("\nFiles:\n");
for name in names.iter().take(40) {
ctx.push_str(&format!(" {name}\n"));
}
}
for readme in ["README.md", "readme.md", "README.txt"] {
let path = std::path::Path::new(workspace).join(readme);
if let Ok(text) = std::fs::read_to_string(&path) {
let excerpt: String = text.chars().take(3000).collect();
ctx.push_str(&format!("\n{readme}:\n{excerpt}\n"));
if text.len() > 3000 {
ctx.push_str("...[truncated]\n");
}
break;
}
}
ctx
}
fn rebuild_system_prompt(
workspace: &str,
memory: &newt_core::MemoryManager,
persona: Option<&Persona>,
conversation_id: &str,
) -> String {
let soul_additions = memory.build_system_prompt_additions();
let soul_text = if soul_additions.is_empty() {
None
} else {
Some(soul_additions.as_str())
};
let plan_path = newt_core::session_plan_path(conversation_id);
build_system_prompt_with_persona(workspace, soul_text, persona, &plan_path.to_string_lossy())
}
fn handle_operating_mode_command(
arg: &str,
active_mode: &mut OperatingMode,
mode_states: &ConversationModeStates,
color: bool,
verbose: bool,
) {
match operating_mode_command_lines(arg, active_mode) {
Ok(mut lines) => {
let normalized = arg.trim().to_ascii_lowercase();
if !matches!(normalized.as_str(), "" | "list" | "show" | "status") {
mode_states.plan.clear();
mode_states.auto.clear();
}
newt_core::operating_mode::set_session_operating_mode(*active_mode);
if let Some(first) = lines.first() {
print_newt(first, color, verbose);
}
for line in lines.drain(1..) {
println!("{line}");
}
}
Err(error) => print_newt(&error, color, verbose),
}
}
fn posture_status_lines(
cfg: &newt_core::Config,
active_posture: Option<&ActivePosture>,
include_available: bool,
) -> Vec<String> {
let active = match active_posture {
Some(posture) if posture.permission_clamp().is_none() => {
format!(
"active permission posture: {} (no permission clamp)",
posture.name
)
}
Some(posture) => format!(
"active permission posture: {} — preset '{}' floor: {}",
posture.name, posture.preset_name, posture.clamp_summary
),
None => "no active permission posture".to_string(),
};
let mut lines = vec![active];
if include_available {
let names: Vec<&str> = cfg.modes.keys().map(String::as_str).collect();
lines.push(if names.is_empty() {
"available permission postures: (none configured — define [modes.<name>] in your newt config)"
.to_string()
} else {
format!("available permission postures: {}", names.join(", "))
});
}
lines
}
fn handle_posture_command(arg: &str, cfg: &newt_core::Config, color: bool, verbose: bool) {
if matches!(arg, "" | "list" | "show" | "status") {
let include_available = matches!(arg, "" | "list");
let mut lines = posture_status_lines(
cfg,
newt_core::posture::active_posture().as_ref(),
include_available,
)
.into_iter();
if let Some(first) = lines.next() {
print_newt(&first, color, verbose);
}
for line in lines {
println!("{line}");
}
return;
}
if matches!(arg, "off" | "clear" | "reset") {
let was_active = newt_core::posture::active_posture().is_some();
newt_core::posture::set_active_posture(None);
if was_active {
print_newt(
"permission posture cleared — authority returns to the session base",
color,
verbose,
);
} else {
print_newt("no active permission posture to clear", color, verbose);
}
return;
}
let skills_dirs = cfg.skill_search_dirs();
let posture = build_posture(arg, cfg, |skill_name| {
newt_skills::load_body_from(&skills_dirs, skill_name)
});
let posture = match posture {
Ok(posture) => posture,
Err(e) => {
print_newt(&format!("error: {e}"), color, verbose);
return;
}
};
if let Some(body) = &posture.skill_body {
print_newt(
&format!("loaded skill for permission posture '{arg}':\n{body}"),
color,
verbose,
);
}
let report = if posture.permission_clamp().is_none() {
format!(
"permission posture '{}' active (no permission clamp)",
posture.name
)
} else {
format!(
"permission posture '{}' active — preset '{}' clamps authority (floor): {}",
posture.name, posture.preset_name, posture.clamp_summary
)
};
newt_core::posture::set_active_posture(Some(posture));
print_newt(&report, color, verbose);
}
fn persona_status(active: Option<&Persona>) -> String {
let Some(persona) = active else {
return "No active persona.".to_string();
};
let mut out = format!(
"Active persona: {} - {} ({})",
persona.name,
persona.description(),
persona.path.display()
);
let profile = &persona.profile;
if let Some(role) = &profile.role {
out.push_str(&format!("\n role: {role}"));
}
match &profile.tools {
Some(tools) if !tools.is_empty() => {
out.push_str(&format!("\n preferred tools: {}", tools.join(", ")));
}
Some(_) => out.push_str("\n preferred tools: (none)"),
None => out.push_str("\n preferred tools: (no preference)"),
}
if let Some(skills) = &profile.skills {
if skills.is_empty() {
out.push_str("\n skills: (none)");
} else {
out.push_str(&format!("\n skills: {}", skills.join(", ")));
}
}
if let Some(caveats) = &profile.caveats {
out.push_str(&format!("\n advisory caveats: {}", caveats.summary()));
}
match (&profile.model, &profile.tier) {
(Some(m), Some(t)) => out.push_str(&format!("\n router: model={m} tier={t:?}")),
(Some(m), None) => out.push_str(&format!("\n router: model={m}")),
(None, Some(t)) => out.push_str(&format!("\n router: tier={t:?}")),
(None, None) => {}
}
if let Some(b) = &profile.backend {
out.push_str(&format!("\n backend: {b}"));
}
if let Some(c) = profile.cognition {
out.push_str(&format!("\n cognition: {}", c.label()));
}
if let Some(i) = profile.initiative {
out.push_str(&format!("\n initiative: {}", i.label()));
}
if profile.crew == Some(true) {
let runtime = if std::env::var("NEWT_TEAM").is_ok() {
"on"
} else {
"off — a launch gate; start with `newt --obsessive` / NEWT_TEAM to engage"
};
out.push_str(&format!("\n crew: declared on · runtime {runtime}"));
}
if !profile.is_role_bound() {
out.push_str("\n (prompt-only persona — no role bindings)");
}
out
}
fn persona_list(store: &PersonaStore) -> anyhow::Result<String> {
store.list_message()
}
struct ConversationResetContext<'a> {
memory: &'a mut newt_core::MemoryManager,
system: &'a mut String,
conversation_id: &'a mut String,
mode_states: &'a ConversationModeStates,
}
fn reset_conversation(
workspace: &str,
active_persona: Option<&Persona>,
ctx: &mut ConversationResetContext<'_>,
) {
ctx.mode_states.clear();
ctx.memory.reset_all();
*ctx.system = rebuild_system_prompt(workspace, ctx.memory, active_persona, ctx.conversation_id);
}
fn new_conversation_message(active_persona: Option<&Persona>) -> String {
match active_persona {
Some(persona) => format!(
"Started a new conversation with persona `{}`.",
persona.name
),
None => "Started a new conversation.".to_string(),
}
}
pub(crate) fn close_out_message(reason: &str, started: &str, outgoing_durable: bool) -> String {
if reason == "end" {
return if outgoing_durable {
"Conversation ended and saved — /resume to reopen it, /exit to leave newt. \
(A fresh conversation is now open.)"
.to_string()
} else {
"Conversation ended — /exit to leave newt. (A fresh conversation is now open.)"
.to_string()
};
}
if !outgoing_durable {
return started.to_string();
}
match reason {
"start" => {
format!("{started} The previous conversation stays open — /resume to return to it.")
}
"new" => started.to_string(),
_ => format!("{started} The previous conversation is saved — /resume to reopen it."),
}
}
pub(crate) struct ConversationScopedState<'a> {
pub scratchpad: &'a dyn newt_core::ScratchpadStore,
pub step_ledger: &'a dyn newt_core::StepLedger,
pub active_prompt_context: &'a mut Option<newt_core::TurnPromptContext>,
}
impl ConversationScopedState<'_> {
pub fn clear(&mut self) {
self.scratchpad.clear();
self.step_ledger.clear();
*self.active_prompt_context = None;
}
}
fn handle_new_conversation(
workspace: &str,
active_persona: Option<&Persona>,
ctx: &mut ConversationResetContext<'_>,
compress_state: &mut newt_core::CompressState,
session_opted_fresh: &mut bool,
scoped: &mut ConversationScopedState<'_>,
) -> String {
*ctx.conversation_id = newt_core::new_conversation_id();
compress_state.reset();
*session_opted_fresh = true;
reset_conversation(workspace, active_persona, ctx);
scoped.clear();
new_conversation_message(active_persona)
}
const EPHEMERAL_SESSION_NOTICE: &str =
"ephemeral session — conversation persistence is off (nothing saved, nothing resumed)";
#[derive(Debug, Clone, PartialEq, Eq)]
enum SessionStart {
Ephemeral,
ResumeExact(String),
ResumeNamed(String),
ResumeLatest,
Fresh,
}
fn resolve_session_start(
ephemeral: bool,
forced_id: Option<String>,
resume_name: Option<String>,
resume_config: bool,
) -> SessionStart {
if ephemeral {
return SessionStart::Ephemeral;
}
if let Some(id) = forced_id {
let id = id.trim().to_string();
if !id.is_empty() {
return SessionStart::ResumeExact(id);
}
}
if let Some(name) = resume_name {
let name = name.trim().to_string();
if !name.is_empty() {
return SessionStart::ResumeNamed(name);
}
}
if resume_config {
SessionStart::ResumeLatest
} else {
SessionStart::Fresh
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum TitleMatch<'a> {
One(&'a newt_core::ConversationSummary),
Ambiguous(Vec<&'a newt_core::ConversationSummary>),
None,
}
fn title_match<'a>(
summaries: &'a [newt_core::ConversationSummary],
needle: &str,
) -> TitleMatch<'a> {
let needle = needle.trim().to_lowercase();
let title_of = |s: &newt_core::ConversationSummary| s.title.trim().to_lowercase();
let exact: Vec<&newt_core::ConversationSummary> =
summaries.iter().filter(|s| title_of(s) == needle).collect();
let hits = if exact.is_empty() {
summaries
.iter()
.filter(|s| title_of(s).contains(needle.as_str()))
.collect::<Vec<_>>()
} else {
exact
};
match hits.as_slice() {
[one] => TitleMatch::One(one),
[] => TitleMatch::None,
many => TitleMatch::Ambiguous(many.to_vec()),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum TitleResolveError {
NotFound { query: String },
Ambiguous {
query: String,
candidates: Vec<(String, String)>,
},
}
impl std::fmt::Display for TitleResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound { query } => write!(
f,
"no conversation titled \"{query}\" in this workspace — run newt and /resume to browse"
),
Self::Ambiguous { query, candidates } => write!(
f,
"\"{query}\" matches {} conversations — use an id: {}",
candidates.len(),
candidates
.iter()
.map(|(id, title)| format!("{} \"{title}\"", short_conversation_id(id)))
.collect::<Vec<_>>()
.join(", ")
),
}
}
}
impl std::error::Error for TitleResolveError {}
fn resolve_conversation_by_name(
summaries: &[newt_core::ConversationSummary],
name: &str,
) -> Result<String, TitleResolveError> {
match title_match(summaries, name) {
TitleMatch::One(one) => Ok(one.id.clone()),
TitleMatch::None => Err(TitleResolveError::NotFound {
query: name.trim().to_string(),
}),
TitleMatch::Ambiguous(many) => Err(TitleResolveError::Ambiguous {
query: name.trim().to_string(),
candidates: many
.iter()
.map(|s| (s.id.clone(), s.title.clone()))
.collect(),
}),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ResumeNameResolve {
Resolved(String),
Ambiguous(Vec<(String, String)>),
NotFound,
}
fn resolve_resume_target(
summaries: &[newt_core::ConversationSummary],
token: &str,
) -> ResumeNameResolve {
let token = token.trim();
if let Some(found) = summaries.iter().find(|s| s.id == token) {
return ResumeNameResolve::Resolved(found.id.clone());
}
let prefix: Vec<&newt_core::ConversationSummary> = summaries
.iter()
.filter(|s| s.id.starts_with(token))
.collect();
if let [one] = prefix.as_slice() {
return ResumeNameResolve::Resolved(one.id.clone());
}
match resolve_conversation_by_name(summaries, token) {
Ok(id) => ResumeNameResolve::Resolved(id),
Err(TitleResolveError::Ambiguous { candidates, .. }) => {
ResumeNameResolve::Ambiguous(candidates)
}
Err(TitleResolveError::NotFound { .. }) => ResumeNameResolve::NotFound,
}
}
fn should_auto_resume(start: &SessionStart, session_opted_fresh: bool) -> bool {
matches!(start, SessionStart::ResumeLatest) && !session_opted_fresh
}
fn conversation_root_dir() -> std::path::PathBuf {
newt_core::Config::user_config_path()
.and_then(|p| p.parent().map(std::path::Path::to_path_buf))
.unwrap_or_else(|| std::path::PathBuf::from(".newt"))
}
fn conversation_store_for(
workspace: &str,
cfg: &newt_core::Config,
) -> anyhow::Result<newt_core::ConversationStore> {
let max_per_workspace = cfg
.conversations
.clone()
.unwrap_or_default()
.max_per_workspace;
newt_core::ConversationStore::new(conversation_root_dir(), workspace, max_per_workspace)
}
fn conversation_title_from_task(task: &str) -> String {
let title: String = task
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.unwrap_or("Untitled conversation")
.chars()
.take(80)
.collect();
if title.trim().is_empty() {
"Untitled conversation".to_string()
} else {
title
}
}
#[derive(Debug)]
enum TurnSaveState {
Durable,
DurableWithAncillaryWarning(anyhow::Error),
Ephemeral,
}
#[allow(clippy::too_many_arguments)]
fn save_successful_conversation_turn(
store: &newt_core::ConversationStore,
conversation_id: &str,
active_persona: Option<&Persona>,
task: &str,
reply: &str,
events: &[newt_core::ToolEvent],
phantom_reaches: &[newt_core::PhantomReach],
usage: Option<newt_core::TokenUsage>,
compaction: Option<String>,
scratchpad: &std::collections::BTreeMap<String, String>,
plan: &newt_core::PlanSnapshot,
) -> anyhow::Result<TurnSaveState> {
save_successful_conversation_turn_with_ancillary(
store,
conversation_id,
active_persona,
task,
reply,
events,
phantom_reaches,
usage,
compaction,
scratchpad,
plan,
|store, conversation_id, scratchpad, plan| {
store
.update_scratchpad(conversation_id, scratchpad)
.context("reply persisted but scratchpad snapshot could not be updated")?;
store
.update_plan_snapshot(conversation_id, plan)
.context("reply persisted but plan snapshot could not be updated")?;
store
.heartbeat(conversation_id)
.context("reply persisted but conversation heartbeat could not be refreshed")
},
)
}
#[allow(clippy::too_many_arguments)]
fn save_successful_conversation_turn_with_ancillary<F>(
store: &newt_core::ConversationStore,
conversation_id: &str,
active_persona: Option<&Persona>,
task: &str,
reply: &str,
events: &[newt_core::ToolEvent],
phantom_reaches: &[newt_core::PhantomReach],
usage: Option<newt_core::TokenUsage>,
compaction: Option<String>,
scratchpad: &std::collections::BTreeMap<String, String>,
plan: &newt_core::PlanSnapshot,
ancillary: F,
) -> anyhow::Result<TurnSaveState>
where
F: FnOnce(
&newt_core::ConversationStore,
&str,
&std::collections::BTreeMap<String, String>,
&newt_core::PlanSnapshot,
) -> anyhow::Result<()>,
{
if !store.exists(conversation_id)? {
store.create_with_id(
conversation_id,
&conversation_title_from_task(task),
active_persona.map(|p| p.name.as_str()),
)?;
}
if let Some(summary) = compaction {
newt_core::persist_compaction_summary(store, conversation_id, &summary)?;
}
let model_reply = newt_core::agentic::model_reply_for_history(reply);
store.append_turn_full(
conversation_id,
task,
&model_reply,
events,
phantom_reaches,
&[],
usage.map(|u| u.input_tokens),
usage.map(|u| u.output_tokens),
)?;
match ancillary(store, conversation_id, scratchpad, plan) {
Ok(()) => Ok(TurnSaveState::Durable),
Err(error) => Ok(TurnSaveState::DurableWithAncillaryWarning(error)),
}
}
#[allow(clippy::too_many_arguments)] fn save_turn_if_persistent(
store: Option<&newt_core::ConversationStore>,
conversation_id: &str,
active_persona: Option<&Persona>,
task: &str,
reply: &str,
events: &[newt_core::ToolEvent],
phantom_reaches: &[newt_core::PhantomReach],
usage: Option<newt_core::TokenUsage>,
compaction: Option<String>,
scratchpad: &std::collections::BTreeMap<String, String>,
plan: &newt_core::PlanSnapshot,
) -> anyhow::Result<TurnSaveState> {
match store {
Some(store) => save_successful_conversation_turn(
store,
conversation_id,
active_persona,
task,
reply,
events,
phantom_reaches,
usage,
compaction,
scratchpad,
plan,
),
None => Ok(TurnSaveState::Ephemeral),
}
}
fn conversation_list_message(store: &newt_core::ConversationStore) -> anyhow::Result<String> {
let summaries = store.list()?;
if summaries.is_empty() {
return Ok("No saved conversations for this workspace.".to_string());
}
let mut out = "Saved conversations:".to_string();
for summary in summaries {
let persona = summary
.persona
.as_deref()
.map(|p| format!(" persona={p}"))
.unwrap_or_default();
out.push_str(&format!(
"\n {} {} ({} turns{})",
summary.id, summary.title, summary.turn_count, persona
));
}
Ok(out)
}
fn conversation_show_message(record: &newt_core::ConversationRecord) -> String {
let persona = record
.persona
.as_deref()
.map(|p| format!("persona: {p}"))
.unwrap_or_else(|| "persona: none".to_string());
let mut out = format!(
"Conversation {}: {}\n{}\nturns: {}",
record.id,
record.title,
persona,
record.turns.len()
);
for (idx, turn) in record.turns.iter().enumerate() {
out.push_str(&format!(
"\n\n{}. user:\n{}\n\nassistant:\n{}",
idx + 1,
turn.user,
turn.assistant
));
}
out
}
struct ConversationCommandContext<'a> {
store: &'a newt_core::ConversationStore,
persona_store: &'a PersonaStore,
workspace: &'a str,
memory: &'a mut newt_core::MemoryManager,
system: &'a mut String,
active_persona: &'a mut Option<Persona>,
active_conversation_id: &'a mut String,
compress_state: &'a mut newt_core::CompressState,
scratchpad: &'a dyn newt_core::ScratchpadStore,
step_ledger: &'a dyn newt_core::StepLedger,
active_prompt_context: &'a mut Option<newt_core::TurnPromptContext>,
mode_states: &'a ConversationModeStates,
}
fn handle_conversation_command(
input: &str,
ctx: &mut ConversationCommandContext<'_>,
) -> anyhow::Result<String> {
match parse_conversation_command(input)? {
ConversationCommand::List => conversation_list_message(ctx.store),
ConversationCommand::Show(id) => {
let record = ctx.store.load(&id)?;
Ok(conversation_show_message(&record))
}
ConversationCommand::Restore(id) => {
let (record, warning) = restore_conversation_into_session(ctx, &id)?;
crate::event_receipt::conversation_op(input, "restore", &record.id);
let mut message = format!(
"Restored conversation `{}` ({} turns).",
record.title,
record.turns.len()
);
if let Some(warning) = warning {
message.push_str(&format!("\nwarning: {warning}"));
}
Ok(message)
}
ConversationCommand::Rename { id, title } => {
let resolved_id = ctx.store.resolve_id(&id)?;
ctx.store.rename(&resolved_id, &title)?;
crate::event_receipt::conversation_op(input, "rename", &resolved_id);
Ok(format!("Renamed conversation `{resolved_id}`."))
}
ConversationCommand::Delete(id) => {
let resolved_id = ctx.store.resolve_id(&id)?;
if *ctx.active_conversation_id == resolved_id {
anyhow::bail!("cannot delete the active conversation; use /new first");
}
ctx.store.delete(&resolved_id)?;
crate::event_receipt::conversation_op(input, "delete", &resolved_id);
Ok(format!("Deleted conversation `{resolved_id}`."))
}
}
}
pub(crate) struct PreparedConversationRestore {
record: newt_core::ConversationRecord,
prompt_context: Option<newt_core::TurnPromptContext>,
persona: Option<Persona>,
persona_warning: Option<String>,
}
#[cfg(test)]
pub(crate) mod restore_prepare_seam {
use std::cell::RefCell;
thread_local! {
static FAILING: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub(crate) fn fail_after_load_for(id: &str) {
FAILING.with(|f| *f.borrow_mut() = Some(id.to_string()));
}
pub(crate) fn clear() {
FAILING.with(|f| *f.borrow_mut() = None);
}
pub(crate) fn forced_failure(id: &str) -> Option<String> {
FAILING.with(|f| {
f.borrow()
.as_deref()
.filter(|failing| *failing == id)
.map(|_| "prompt receipt context failed to validate (test seam)".to_string())
})
}
}
pub(crate) fn prepare_conversation_restore(
store: &newt_core::ConversationStore,
persona_store: &PersonaStore,
id: &str,
) -> anyhow::Result<PreparedConversationRestore> {
let record = store.load_verified(id)?;
#[cfg(test)]
if let Some(e) = restore_prepare_seam::forced_failure(id) {
anyhow::bail!(e);
}
let prompt_context = match store.latest_prompt(&record.id)? {
Some(receipt) => store.turn_prompt_context(&record.id, receipt.id())?,
None => None,
};
let (persona, persona_warning) = match record.persona.as_deref() {
Some(name) => match persona_store.load(name) {
Ok(persona) => (Some(persona), None),
Err(e) => (None, Some(format!("persona `{name}` unavailable: {e}"))),
},
None => (None, None),
};
Ok(PreparedConversationRestore {
record,
prompt_context,
persona,
persona_warning,
})
}
pub(crate) struct CommitContext<'a> {
pub workspace: &'a str,
pub memory: &'a mut newt_core::MemoryManager,
pub system: &'a mut String,
pub active_persona: &'a mut Option<Persona>,
pub active_conversation_id: &'a mut String,
pub compress_state: &'a mut newt_core::CompressState,
pub scratchpad: &'a dyn newt_core::ScratchpadStore,
pub step_ledger: &'a dyn newt_core::StepLedger,
pub active_prompt_context: &'a mut Option<newt_core::TurnPromptContext>,
pub mode_states: &'a ConversationModeStates,
}
impl<'a> ConversationCommandContext<'a> {
pub(crate) fn commit_context(&mut self) -> CommitContext<'_> {
CommitContext {
workspace: self.workspace,
memory: self.memory,
system: self.system,
active_persona: self.active_persona,
active_conversation_id: self.active_conversation_id,
compress_state: self.compress_state,
scratchpad: self.scratchpad,
step_ledger: self.step_ledger,
active_prompt_context: self.active_prompt_context,
mode_states: self.mode_states,
}
}
}
pub(crate) fn commit_conversation_restore(
ctx: &mut CommitContext<'_>,
prepared: PreparedConversationRestore,
) -> (newt_core::ConversationRecord, Option<String>) {
let PreparedConversationRestore {
record,
prompt_context,
persona,
persona_warning,
} = prepared;
commit_prepared(ctx, record, prompt_context, persona, persona_warning)
}
fn restore_conversation_into_session(
ctx: &mut ConversationCommandContext<'_>,
id: &str,
) -> anyhow::Result<(newt_core::ConversationRecord, Option<String>)> {
let prepared = prepare_conversation_restore(ctx.store, ctx.persona_store, id)?;
Ok(commit_conversation_restore(
&mut ctx.commit_context(),
prepared,
))
}
fn commit_prepared(
ctx: &mut CommitContext<'_>,
record: newt_core::ConversationRecord,
restored_prompt_context: Option<newt_core::TurnPromptContext>,
prepared_persona: Option<Persona>,
warning: Option<String>,
) -> (newt_core::ConversationRecord, Option<String>) {
ctx.mode_states.clear();
ctx.memory.restore_turns(&record.turns);
ctx.scratchpad.clear();
for (key, value) in &record.scratchpad {
ctx.scratchpad.set(key, value.clone());
}
ctx.step_ledger.restore(&record.plan);
*ctx.active_prompt_context = restored_prompt_context;
*ctx.active_persona = prepared_persona;
seat_persona_dials(ctx.active_persona.as_ref());
ctx.compress_state.reset();
*ctx.active_conversation_id = record.id.clone();
*ctx.system = rebuild_system_prompt(
ctx.workspace,
ctx.memory,
ctx.active_persona.as_ref(),
ctx.active_conversation_id,
);
(record, warning)
}
fn resume_session_conversation(
ctx: &mut ConversationCommandContext<'_>,
id: &str,
) -> anyhow::Result<String> {
let (record, warning) = restore_conversation_into_session(ctx, id)?;
let title = recall_display_title(ctx.store, &record.id, &record.title);
Ok(auto_resume_banner(&record, &title, warning.as_deref()))
}
fn auto_resume_latest(ctx: &mut ConversationCommandContext<'_>) -> anyhow::Result<Option<String>> {
let Some(latest) = ctx.store.latest_open()? else {
return Ok(None);
};
resume_session_conversation(ctx, &latest.id).map(Some)
}
fn resume_exact_conversation(
ctx: &mut ConversationCommandContext<'_>,
id: &str,
) -> anyhow::Result<String> {
if !ctx.store.exists(id)? {
anyhow::bail!(
"NEWT_CONVERSATION_ID: conversation `{id}` does not exist in this \
workspace (the workspace fence applies — a conversation belonging \
to another workspace cannot be resumed here)"
);
}
resume_session_conversation(ctx, id)
}
fn auto_resume_banner(
record: &newt_core::ConversationRecord,
display_title: &str,
warning: Option<&str>,
) -> String {
let mut banner = format!(
"resumed conversation {} {} ({} turns, last active ~{}) — /new starts fresh",
short_conversation_id(&record.id),
display_title,
record.turns.len(),
claim_timestamp(record.updated_at_unix_nanos),
);
let restored_keys = record.scratchpad.len();
if restored_keys > 0 {
banner.push_str(&format!(
" — restored {restored_keys} <state> key{}",
if restored_keys == 1 { "" } else { "s" }
));
if let Some(task) = record.scratchpad.get("current_task") {
let task = task.trim();
if !task.is_empty() {
const MAX: usize = 80;
let shown: String = task.chars().take(MAX).collect();
let ellipsis = if task.chars().count() > MAX {
"…"
} else {
""
};
banner.push_str(&format!(" — last task: {shown}{ellipsis}"));
}
}
}
let restored_steps = record.plan.len();
if restored_steps > 0 {
banner.push_str(&format!(
" — restored plan ({restored_steps} step{})",
if restored_steps == 1 { "" } else { "s" }
));
}
if let Some(warning) = warning {
banner.push_str(&format!("\nwarning: {warning}"));
}
banner
}
const RECALL_LIMIT: usize = 10;
fn recall_browse_message(store: &newt_core::ConversationStore) -> anyhow::Result<String> {
let mut summaries = store.list()?;
if summaries.is_empty() {
return Ok("No saved conversations for this workspace.".to_string());
}
summaries.reverse(); let total = summaries.len();
let mut out = String::from("Recent conversations (most recent first):");
for summary in summaries.iter().take(RECALL_LIMIT) {
out.push_str(&format!(
"\n {} {} ({} turns, last active ~{})",
short_conversation_id(&summary.id),
recall_display_title(store, &summary.id, &summary.title),
summary.turn_count,
claim_timestamp(summary.updated_at_unix_nanos),
));
}
if total > RECALL_LIMIT {
out.push_str(&format!(
"\n … {} more — /conversation list shows all.",
total - RECALL_LIMIT
));
}
out.push_str("\nRestore with /conversation restore <id>.");
Ok(out)
}
fn recall_search_message(
store: &newt_core::ConversationStore,
query: &str,
) -> anyhow::Result<String> {
if newt_core::sanitize_fts5_query(query).is_err() {
return Ok(format!(
"Nothing searchable in `{query}` — every term was FTS5 syntax or \
punctuation. Try plain keywords, e.g. /recall tokio panic."
));
}
let hits = store.search(query, RECALL_LIMIT)?;
if hits.is_empty() {
return Ok(format!(
"No matches for `{query}` in this workspace's conversations."
));
}
let mut out = format!("Recall matches for `{query}`:");
for hit in &hits {
out.push_str(&format!(
"\n {} {} · seq {}\n {}",
short_conversation_id(&hit.conversation_id),
recall_display_title(store, &hit.conversation_id, &hit.title),
hit.seq,
readable_snippet(&hit.snippet),
));
}
out.push_str("\nRestore with /conversation restore <id>.");
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ResumeCommand {
Browse,
Select(usize),
Query(String),
Find(String),
}
fn parse_resume_command(input: &str) -> ResumeCommand {
let body = input.trim().trim_start_matches('/').trim();
if let Some(rest) = body.strip_prefix("recall") {
if rest.is_empty() || rest.starts_with(char::is_whitespace) {
return ResumeCommand::Find(rest.trim().to_string());
}
}
let rest = body.strip_prefix("resume").map(str::trim).unwrap_or("");
if rest.is_empty() {
return ResumeCommand::Browse;
}
if let Some(query) = rest.strip_prefix("find") {
if query.is_empty() || query.starts_with(char::is_whitespace) {
return ResumeCommand::Find(query.trim().to_string());
}
}
if let Ok(n) = rest.parse::<usize>() {
if (1..=RECALL_LIMIT).contains(&n) {
return ResumeCommand::Select(n);
}
}
ResumeCommand::Query(rest.to_string())
}
fn resume_liveness_marker(
store: &newt_core::ConversationStore,
id: &str,
active_id: &str,
) -> &'static str {
if id == active_id {
return "▶";
}
match store.live_owner(id) {
Ok(Some(owner)) if store.is_owner_live(&owner) => "●",
_ => "○",
}
}
const RESUME_LEGEND: &str = "\n ▶ current · ● open in another newt · ○ resumable — \
reopen with /resume <n> or /resume <id>.";
fn resume_browse_message(
store: &newt_core::ConversationStore,
active_id: &str,
) -> anyhow::Result<(String, Vec<String>)> {
let mut summaries = store.list()?;
if summaries.is_empty() {
return Ok((
"No saved conversations for this workspace.".to_string(),
Vec::new(),
));
}
summaries.reverse(); let total = summaries.len();
let mut out = String::from("Conversations (most recent first):");
let mut ids = Vec::new();
for (i, s) in summaries.iter().take(RECALL_LIMIT).enumerate() {
out.push_str(&format!(
"\n {:>2}. {} {} {} ({} turns, ~{})",
i + 1,
resume_liveness_marker(store, &s.id, active_id),
short_conversation_id(&s.id),
recall_display_title(store, &s.id, &s.title),
s.turn_count,
claim_timestamp(s.updated_at_unix_nanos),
));
ids.push(s.id.clone());
}
if total > RECALL_LIMIT {
out.push_str(&format!(
"\n … {} more — refine with /resume <query>.",
total - RECALL_LIMIT
));
}
out.push_str(RESUME_LEGEND);
Ok((out, ids))
}
fn resume_search_message(
store: &newt_core::ConversationStore,
query: &str,
active_id: &str,
) -> anyhow::Result<(String, Vec<String>)> {
if newt_core::sanitize_fts5_query(query).is_err() {
return Ok((
format!(
"Nothing searchable in `{query}` — every term was FTS5 syntax or \
punctuation. Try plain keywords, e.g. /resume tokio panic."
),
Vec::new(),
));
}
let hits = store.search(query, RECALL_LIMIT)?;
if hits.is_empty() {
return Ok((
format!("No matches for `{query}` in this workspace's conversations."),
Vec::new(),
));
}
let mut out = format!("Matches for `{query}`:");
let mut ids: Vec<String> = Vec::new();
for hit in &hits {
if ids.iter().any(|existing| existing == &hit.conversation_id) {
continue; }
out.push_str(&format!(
"\n {:>2}. {} {} {}\n {}",
ids.len() + 1,
resume_liveness_marker(store, &hit.conversation_id, active_id),
short_conversation_id(&hit.conversation_id),
recall_display_title(store, &hit.conversation_id, &hit.title),
readable_snippet(&hit.snippet),
));
ids.push(hit.conversation_id.clone());
}
out.push_str(RESUME_LEGEND);
Ok((out, ids))
}
fn resume_ambiguous_message(
store: &newt_core::ConversationStore,
query: &str,
candidates: &[(String, String)],
active_id: &str,
) -> anyhow::Result<(String, Vec<String>)> {
let mut out = format!(
"\"{query}\" matches {} conversations — pick one:",
candidates.len()
);
let mut ids = Vec::new();
for (i, (id, title)) in candidates.iter().enumerate() {
out.push_str(&format!(
"\n {:>2}. {} {} {}",
i + 1,
resume_liveness_marker(store, id, active_id),
short_conversation_id(id),
recall_display_title(store, id, title),
));
ids.push(id.clone());
}
out.push_str(RESUME_LEGEND);
Ok((out, ids))
}
fn readable_snippet(snippet: &str) -> String {
snippet
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.replace(">>>", "«")
.replace("<<<", "»")
}
fn short_conversation_id(id: &str) -> &str {
id.get(..12).unwrap_or(id)
}
fn claim_timestamp(unix_nanos: u128) -> String {
i64::try_from(unix_nanos / 1_000_000_000)
.ok()
.and_then(|secs| chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0))
.map(|dt| dt.format("%Y-%m-%d %H:%M UTC").to_string())
.unwrap_or_else(|| "unknown".to_string())
}
fn recall_display_title(store: &newt_core::ConversationStore, id: &str, title: &str) -> String {
let trimmed = title.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
if let Ok(record) = store.load(id) {
if let Some(turn) = record.turns.first() {
let fallback: String = turn
.user
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(60)
.collect();
if !fallback.is_empty() {
return fallback;
}
}
}
"(untitled)".to_string()
}
fn parse_compress_command(input: &str) -> anyhow::Result<Option<String>> {
let body = input.trim().trim_start_matches('/').trim();
let Some(rest) = body
.strip_prefix("compress")
.or_else(|| body.strip_prefix("compact"))
else {
anyhow::bail!("not a compress command");
};
if !rest.is_empty() && !rest.starts_with(char::is_whitespace) {
anyhow::bail!("not a compress command");
}
let focus = rest.trim();
Ok((!focus.is_empty()).then(|| focus.to_string()))
}
fn session_wire_view(memory: &newt_core::MemoryManager, system: &str) -> Vec<serde_json::Value> {
let mut wire: Vec<serde_json::Value> = memory
.build_messages(system, "")
.iter()
.map(|m| serde_json::json!({"role": m.role.as_str(), "content": m.content}))
.collect();
if wire
.last()
.is_some_and(|m| m["role"] == "user" && m["content"] == "")
{
wire.pop();
}
wire
}
fn wire_messages_to_turns(messages: &[serde_json::Value]) -> Vec<newt_core::ConversationTurn> {
let mut out: Vec<newt_core::ConversationTurn> = Vec::new();
for m in messages {
let content = m["content"].as_str().unwrap_or_default();
match m["role"].as_str() {
Some("user") => out.push(newt_core::ConversationTurn::new(content, "")),
Some("assistant") => match out.last_mut() {
Some(last)
if last.assistant.is_empty()
&& !last.user.is_empty()
&& !last.user.starts_with(newt_core::agentic::SUMMARY_PREFIX) =>
{
last.assistant = content.to_string();
}
_ => out.push(newt_core::ConversationTurn::new("", content)),
},
_ => {}
}
}
out
}
fn compress_feedback_message(outcome: &newt_core::ManualCompressOutcome) -> String {
if !outcome.fired {
return format!(
"no compression possible — {} message(s), ~{} est. tokens are already \
protected head/tail or unprunable",
outcome.messages_before, outcome.tokens_before
);
}
let mut msg = format!(
"context compressed: {} → {} messages, ~{} → ~{} est. tokens ({})",
outcome.messages_before,
outcome.messages_after,
outcome.tokens_before,
outcome.tokens_after,
outcome.how
);
if outcome.tokens_after >= outcome.tokens_before {
msg.push_str(
"\nnote: no token savings — fewer messages can still raise the \
estimate (the transcript was rewritten into denser summaries)",
);
}
msg
}
fn memory_compress_section(c: &newt_core::CompressCounters) -> String {
let mut line = format!(" compressions this session: {}", c.compressions);
if let Some(reclaim) = c.last_reclaim {
if reclaim >= 0.0 {
line.push_str(&format!(" (last reclaimed {:.0}%)", reclaim * 100.0));
} else {
line.push_str(&format!(
" (last pass grew the estimate {:.0}%)",
-reclaim * 100.0
));
}
}
let strikes = format!(
" ineffective-pass strikes: {}/2 (two latch the disable)",
c.strikes
);
let status = if c.disabled {
" auto-compression: disabled — anti-thrash latched; /new resets it"
} else {
" auto-compression: enabled"
};
format!("{line}\n{strikes}\n{status}")
}
fn wal_fallback_startup_notice(notice: Option<&str>) -> Option<String> {
notice.map(|cause| {
format!(
"conversation store: SQLite WAL unavailable, using the journal_mode=DELETE \
fallback (typical for NFS homes; concurrent newts may wait on locks). \
Cause: {cause}"
)
})
}
fn handle_persona_command(
input: &str,
workspace: &str,
store: &PersonaStore,
active_persona: &mut Option<Persona>,
ctx: &mut ConversationResetContext<'_>,
) -> anyhow::Result<String> {
match parse_persona_command(input)? {
PersonaCommand::List => persona_list(store),
PersonaCommand::Show => Ok(persona_status(active_persona.as_ref())),
PersonaCommand::Clear => {
*active_persona = None;
seat_persona_dials(None);
*ctx.conversation_id = newt_core::new_conversation_id();
reset_conversation(workspace, active_persona.as_ref(), ctx);
Ok("Started a new conversation with no active persona.".to_string())
}
PersonaCommand::Set { name, keep_context } => {
let persona = store.load(&name)?;
seat_persona_dials(Some(&persona));
*active_persona = Some(persona);
if keep_context {
*ctx.system = rebuild_system_prompt(
workspace,
ctx.memory,
active_persona.as_ref(),
ctx.conversation_id,
);
Ok(persona_swap_kept_context_message(active_persona.as_ref()))
} else {
*ctx.conversation_id = newt_core::new_conversation_id();
reset_conversation(workspace, active_persona.as_ref(), ctx);
Ok(new_conversation_message(active_persona.as_ref()))
}
}
}
}
fn persona_swap_kept_context_message(active_persona: Option<&Persona>) -> String {
match active_persona {
Some(persona) => format!(
"Switched to persona `{}` (kept conversation context).",
persona.name
),
None => "Switched persona (kept conversation context).".to_string(),
}
}
fn effective_mid_loop_trim_tokens(
model_override: Option<usize>,
global: Option<usize>,
) -> Option<usize> {
model_override.or(global).filter(|&t| t > 0)
}
fn tool_output_lines(cfg: &newt_core::Config) -> usize {
cfg.tui.as_ref().map(|t| t.tool_output_lines).unwrap_or(20)
}
fn spill_lines(cfg: &newt_core::Config) -> usize {
cfg.tui.as_ref().map(|t| t.spill_lines).unwrap_or(3)
}
fn time_marker_secs(cfg: &newt_core::Config) -> u64 {
cfg.tui.as_ref().map(|t| t.time_marker_secs).unwrap_or(300)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SpillCommand {
Status,
Set(usize),
Reset,
Summary,
Excerpt,
Last,
Open(u64),
}
fn apply_spill_command(
cmd: SpillCommand,
lines_override: &mut Option<usize>,
summary_override: &mut Option<bool>,
) {
match cmd {
SpillCommand::Status => {}
SpillCommand::Set(rows) => *lines_override = Some(rows),
SpillCommand::Summary => *summary_override = Some(true),
SpillCommand::Excerpt => *summary_override = Some(false),
SpillCommand::Last | SpillCommand::Open(_) => {}
SpillCommand::Reset => {
*lines_override = None;
*summary_override = None;
}
}
}
fn parse_spill_command(input: &str) -> anyhow::Result<SpillCommand> {
let body = input.trim().trim_start_matches('/').trim();
let Some(rest) = body.strip_prefix("spill") else {
anyhow::bail!("not a spill command");
};
if !rest.is_empty()
&& !rest
.chars()
.next()
.map(char::is_whitespace)
.unwrap_or(false)
{
anyhow::bail!("not a spill command");
}
let arg = rest.trim();
let normalized = arg.to_ascii_lowercase();
if let Some(value) = normalized.strip_prefix("open ") {
let id = value
.trim()
.parse::<u64>()
.map_err(|_| anyhow::anyhow!("/spill open needs a positive result id"))?;
if id == 0 || value.split_whitespace().count() != 1 {
anyhow::bail!("/spill open needs one positive result id");
}
return Ok(SpillCommand::Open(id));
}
match normalized.as_str() {
"" | "show" | "status" => Ok(SpillCommand::Status),
"reset" | "default" | "config" | "auto" => Ok(SpillCommand::Reset),
"summary" | "collapse" => Ok(SpillCommand::Summary),
"excerpt" | "expand" | "rows" => Ok(SpillCommand::Excerpt),
"last" => Ok(SpillCommand::Last),
_ => arg
.parse::<usize>()
.map(SpillCommand::Set)
.map_err(|_| anyhow::anyhow!("unknown /spill argument '{arg}'")),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum SearchCommand {
Query(String),
Preview(usize),
Model,
Rejects,
Pin(usize),
Exclude(usize),
Status,
Clear,
Help,
}
fn parse_search_command(input: &str) -> anyhow::Result<SearchCommand> {
let body = input.trim().trim_start_matches('/').trim();
let Some(rest) = body.strip_prefix("search") else {
anyhow::bail!("not a search command");
};
if !rest.is_empty()
&& !rest
.chars()
.next()
.map(char::is_whitespace)
.unwrap_or(false)
{
anyhow::bail!("not a search command");
}
let arg = rest.trim();
if arg.is_empty() || matches!(arg, "help" | "--help" | "-h") {
return Ok(SearchCommand::Help);
}
let (verb, tail) = match arg.split_once(char::is_whitespace) {
Some((v, t)) => (v, t.trim()),
None => (arg, ""),
};
match verb {
"preview" => Ok(SearchCommand::Preview(if tail.is_empty() {
1
} else {
tail.parse()
.map_err(|_| anyhow::anyhow!("usage: /search preview [N]"))?
})),
"model" | "packet" => Ok(SearchCommand::Model),
"rejects" | "reject" | "ledger" => Ok(SearchCommand::Rejects),
"pin" => {
let n: usize = tail
.parse()
.map_err(|_| anyhow::anyhow!("usage: /search pin <N>"))?;
Ok(SearchCommand::Pin(n))
}
"exclude" | "x" => {
let n: usize = tail
.parse()
.map_err(|_| anyhow::anyhow!("usage: /search exclude <N>"))?;
Ok(SearchCommand::Exclude(n))
}
"status" => Ok(SearchCommand::Status),
"clear" => Ok(SearchCommand::Clear),
_ => Ok(SearchCommand::Query(arg.to_string())),
}
}
fn search_help_text() -> &'static str {
"/search <query> — semantic search (shared with model code_search)\n\
/search preview [N] — preview hit N (default 1)\n\
/search model — exact <code_evidence> packet the model would see\n\
/search rejects — reject ledger (below top_k / budget / excluded)\n\
/search pin N — pin hit N into the next inject + tool retrieve\n\
/search exclude N — exclude hit N's path from automatic retrieval\n\
/search status — index generation, completeness, git HEAD/dirty\n\
/search clear — clear session pins/exclusions"
}
fn lightweight_git_meta(
workspace: &str,
read_scope: &newt_core::Scope<String>,
) -> (Option<String>, Option<bool>) {
let head = newt_core::git_hardening::metadata_git(
std::path::Path::new(workspace),
&["rev-parse", "HEAD"],
read_scope,
)
.and_then(|mut command| command.output())
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let dirty = newt_core::git_hardening::metadata_git(
std::path::Path::new(workspace),
&["status", "--porcelain"],
read_scope,
)
.and_then(|mut command| command.output())
.ok()
.filter(|o| o.status.success())
.map(|o| !o.stdout.is_empty());
(head, dirty)
}
fn effective_spill_lines(configured: usize, session_override: Option<usize>) -> usize {
session_override.unwrap_or(configured)
}
fn committed_spill_lines(surface_is_rich: bool, configured: usize) -> usize {
if surface_is_rich {
configured
} else {
0
}
}
fn effective_spill_summary(surface_is_rich: bool, session_override: Option<bool>) -> bool {
session_override.unwrap_or(surface_is_rich)
}
fn initial_spill_override(trace: bool) -> Option<usize> {
trace.then_some(0)
}
fn toggle_spill_detail(current: Option<usize>, configured: usize) -> Option<usize> {
if effective_spill_lines(configured, current) == 0 {
Some(configured.max(1))
} else {
Some(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SpillEligibility {
Available,
UnsupportedPlatform,
FeatureDisabled,
StdinNotTty,
StdoutNotTty,
TermDumb,
TerminalOwnedByCockpit,
}
impl SpillEligibility {
fn explain(self) -> &'static str {
match self {
Self::Available => "live interaction available",
Self::UnsupportedPlatform => {
"live interaction unavailable: unsupported platform (needs a POSIX terminal)"
}
Self::FeatureDisabled => {
"live interaction unavailable: built without the `live-spill` feature \
(this is the lean/wyvern build)"
}
Self::StdinNotTty => {
"live interaction unavailable: stdin is not a terminal (piped or redirected)"
}
Self::StdoutNotTty => {
"live interaction unavailable: stdout is not a terminal (piped or redirected)"
}
Self::TermDumb => "live interaction unavailable: TERM=dumb disclaims cursor control",
Self::TerminalOwnedByCockpit => {
"live interaction unavailable: the cockpit owns the terminal \
(committed results stay full; /spill open <id> reopens one)"
}
}
}
}
fn spill_status(
configured: usize,
session_override: Option<usize>,
summary: bool,
surface_is_rich: bool,
eligibility: SpillEligibility,
) -> String {
let effective = effective_spill_lines(configured, session_override);
let committed = committed_spill_lines(surface_is_rich, effective);
let rows = if effective == 0 {
"unbounded".to_string()
} else {
effective.to_string()
};
let source = match session_override {
Some(_) => format!(" this session (config default {configured}"),
None => " (config default".to_string(),
};
let live = if effective == 0 {
"live viewport disabled: spill_lines is 0 (/spill <n> raises it)"
} else {
eligibility.explain()
};
let lean_note = if !surface_is_rich && effective != 0 {
"; committed results always print in full on this surface"
} else {
""
};
let mode = if summary && committed != 0 {
"; results collapse to a summary line (/spill excerpt restores rows)"
} else {
""
};
format!("spill rows: {rows}{source}; {live}{lean_note}{mode})")
}
fn summary_recovery_available(surface_is_rich: bool, terminal_owns_turn: bool) -> bool {
#[cfg(all(feature = "rich-tui", feature = "live-spill"))]
{
surface_is_rich && live_spill_capable(terminal_owns_turn)
}
#[cfg(not(all(feature = "rich-tui", feature = "live-spill")))]
{
let _ = surface_is_rich;
let _ = terminal_owns_turn;
false
}
}
fn live_spill_eligibility(terminal_owns_turn: bool) -> SpillEligibility {
let term = std::env::var("TERM").ok();
spill_eligibility_for(
cfg!(unix),
cfg!(feature = "live-spill"),
std::io::stdin().is_terminal(),
std::io::stdout().is_terminal(),
term.as_deref(),
terminal_owns_turn,
)
}
#[cfg(feature = "live-spill")]
fn live_spill_capable(terminal_owns_turn: bool) -> bool {
live_spill_eligibility(terminal_owns_turn) == SpillEligibility::Available
}
fn spill_eligibility_for(
platform_supported: bool,
feature_enabled: bool,
stdin_terminal: bool,
stdout_terminal: bool,
term: Option<&str>,
terminal_owns_turn: bool,
) -> SpillEligibility {
if !platform_supported {
SpillEligibility::UnsupportedPlatform
} else if !feature_enabled {
SpillEligibility::FeatureDisabled
} else if !stdin_terminal {
SpillEligibility::StdinNotTty
} else if !stdout_terminal {
SpillEligibility::StdoutNotTty
} else if term == Some("dumb") {
SpillEligibility::TermDumb
} else if terminal_owns_turn {
SpillEligibility::TerminalOwnedByCockpit
} else {
SpillEligibility::Available
}
}
#[cfg(any(feature = "live-spill", test))]
fn live_spill_capable_for(
platform_supported: bool,
feature_enabled: bool,
stdin_terminal: bool,
stdout_terminal: bool,
term: Option<&str>,
terminal_owns_turn: bool,
) -> bool {
spill_eligibility_for(
platform_supported,
feature_enabled,
stdin_terminal,
stdout_terminal,
term,
terminal_owns_turn,
) == SpillEligibility::Available
}
#[cfg(feature = "live-spill")]
fn mouse_viewport(cfg: &newt_core::Config) -> bool {
if let Ok(v) = std::env::var("NEWT_MOUSE") {
return matches!(v.as_str(), "1" | "true" | "on" | "yes");
}
cfg.tui.as_ref().map(|t| t.mouse_viewport).unwrap_or(false)
}
#[cfg(feature = "live-spill")]
fn mouse_capable(opt_in: bool) -> bool {
let term = std::env::var("TERM").ok();
mouse_capable_for(
cfg!(unix),
cfg!(feature = "live-spill"),
std::io::stdin().is_terminal(),
std::io::stdout().is_terminal(),
term.as_deref(),
opt_in,
)
}
#[cfg(feature = "live-spill")]
fn mouse_capable_for(
platform_supported: bool,
feature_enabled: bool,
stdin_terminal: bool,
stdout_terminal: bool,
term: Option<&str>,
opt_in: bool,
) -> bool {
opt_in
&& live_spill_capable_for(
platform_supported,
feature_enabled,
stdin_terminal,
stdout_terminal,
term,
false,
)
}
fn max_tool_rounds(cfg: &newt_core::Config) -> usize {
cfg.tui
.as_ref()
.map(|t| t.max_tool_rounds)
.unwrap_or_else(|| newt_core::TuiConfig::default().max_tool_rounds)
}
fn workflow_grace_rounds(cfg: &newt_core::Config) -> usize {
cfg.tui
.as_ref()
.map(|t| t.workflow_grace_rounds)
.unwrap_or(5)
}
fn narration_nudge_cap(cfg: &newt_core::Config) -> usize {
cfg.tui.as_ref().map(|t| t.narration_nudge_cap).unwrap_or(1)
}
const EFFECTIVELY_UNLIMITED_TOOL_ROUNDS: usize = newt_core::tenacity::RELENTLESS_TOOL_ROUND_TARGET;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ToolRoundLimitCommand {
Show,
Set(usize),
Double,
Reset,
Configured,
Unlimited,
}
fn tool_round_limit_command(input: &str) -> Option<(&'static str, &str)> {
let body = input.trim().trim_start_matches('/').trim();
["rounds", "tool-rounds", "max-rounds"]
.iter()
.find_map(|cmd| {
let rest = body.strip_prefix(*cmd)?;
let boundary = rest.is_empty()
|| rest
.chars()
.next()
.map(char::is_whitespace)
.unwrap_or(false);
boundary.then(|| (*cmd, rest.trim()))
})
}
fn tool_round_limit_command_arg(input: &str) -> Option<&str> {
tool_round_limit_command(input).map(|(_, arg)| arg)
}
fn parse_tool_round_limit_command(input: &str) -> anyhow::Result<ToolRoundLimitCommand> {
let Some(arg) = tool_round_limit_command_arg(input) else {
anyhow::bail!("not a tool-round limit command");
};
if arg.is_empty() {
return Ok(ToolRoundLimitCommand::Show);
}
let normalized = arg.to_ascii_lowercase();
match normalized.as_str() {
"show" | "status" => Ok(ToolRoundLimitCommand::Show),
"double" | "x2" | "2x" => Ok(ToolRoundLimitCommand::Double),
"reset" | "default" | "auto" => Ok(ToolRoundLimitCommand::Reset),
"config" | "configured" => Ok(ToolRoundLimitCommand::Configured),
"unlimited" | "infinite" | "finish" | "until-finished" | "run-until-finished"
| "until finished" | "run until finished" => Ok(ToolRoundLimitCommand::Unlimited),
_ => {
let n = arg.parse::<usize>().map_err(|_| {
anyhow::anyhow!(
"unknown /rounds argument '{arg}' (use show, <n>, double, reset, config, unlimited)"
)
})?;
if n == 0 {
anyhow::bail!("tool-call round limit must be at least 1");
}
if n > EFFECTIVELY_UNLIMITED_TOOL_ROUNDS {
anyhow::bail!(
"tool-call round limit must be <= {EFFECTIVELY_UNLIMITED_TOOL_ROUNDS}"
);
}
Ok(ToolRoundLimitCommand::Set(n))
}
}
}
fn tenacity_tool_round_limit(
configured: usize,
explicit_tenacity: Option<newt_core::Tenacity>,
) -> usize {
newt_core::tenacity::resolve_tool_round_limit(configured, explicit_tenacity, None).rounds
}
fn effective_tool_round_limit(
configured: usize,
explicit_tenacity: Option<newt_core::Tenacity>,
session_override: Option<usize>,
) -> newt_core::tenacity::ToolRoundLimit {
newt_core::tenacity::resolve_tool_round_limit(configured, explicit_tenacity, session_override)
}
fn double_tool_round_limit(current: usize) -> usize {
if current >= EFFECTIVELY_UNLIMITED_TOOL_ROUNDS {
current
} else {
current
.saturating_mul(2)
.clamp(1, EFFECTIVELY_UNLIMITED_TOOL_ROUNDS)
}
}
fn apply_tool_round_limit_command(
configured: usize,
explicit_tenacity: Option<newt_core::Tenacity>,
session_override: Option<usize>,
command: ToolRoundLimitCommand,
) -> Option<usize> {
match command {
ToolRoundLimitCommand::Show => session_override,
ToolRoundLimitCommand::Set(rounds) => Some(rounds),
ToolRoundLimitCommand::Double => Some(double_tool_round_limit(
effective_tool_round_limit(configured, explicit_tenacity, session_override).rounds,
)),
ToolRoundLimitCommand::Reset => None,
ToolRoundLimitCommand::Configured => Some(configured),
ToolRoundLimitCommand::Unlimited => Some(
effective_tool_round_limit(configured, explicit_tenacity, session_override)
.rounds
.max(EFFECTIVELY_UNLIMITED_TOOL_ROUNDS),
),
}
}
fn describe_tool_round_limit(rounds: usize) -> String {
if rounds >= EFFECTIVELY_UNLIMITED_TOOL_ROUNDS {
format!("{rounds} (effectively unlimited)")
} else {
rounds.to_string()
}
}
fn tool_round_limit_status(
configured: usize,
explicit_tenacity: Option<newt_core::Tenacity>,
session_override: Option<usize>,
) -> String {
let posture_default = tenacity_tool_round_limit(configured, explicit_tenacity);
let explicit_relentless = explicit_tenacity == Some(newt_core::Tenacity::Relentless);
match session_override {
Some(rounds) if explicit_relentless => format!(
"initial tool-call round allowance: {} this session (explicit relentless tenacity default {}; config/model default {})",
describe_tool_round_limit(rounds),
describe_tool_round_limit(posture_default),
describe_tool_round_limit(configured),
),
Some(rounds) => format!(
"initial tool-call round allowance: {} this session (config/model default {})",
describe_tool_round_limit(rounds),
describe_tool_round_limit(configured),
),
None if explicit_relentless => format!(
"initial tool-call round allowance: {posture_default} (effectively unlimited; explicit relentless tenacity; config/model default {})",
describe_tool_round_limit(configured),
),
None => format!(
"initial tool-call round allowance: {} (config/model default)",
describe_tool_round_limit(configured)
),
}
}
#[cfg_attr(not(feature = "rich-tui"), allow(dead_code))]
fn psyche_apply_summary(
runtime_summary: &str,
configured: usize,
explicit_tenacity: Option<newt_core::Tenacity>,
session_override: Option<usize>,
) -> String {
format!(
"{runtime_summary}\n{}",
tool_round_limit_status(configured, explicit_tenacity, session_override)
)
}
fn num_ctx(cfg: &newt_core::Config) -> Option<u32> {
if let Ok(val) = std::env::var("NEWT_NUM_CTX") {
if let Ok(n) = val.trim().parse::<u32>() {
return Some(n);
}
}
cfg.tui.as_ref().and_then(|t| t.num_ctx)
}
fn real_context_discovery(cfg: &newt_core::Config, model: &str) -> bool {
cfg.find_model_tuning(model)
.and_then(|t| t.real_context_discovery)
.or_else(|| cfg.tui.as_ref().and_then(|t| t.real_context_discovery))
.unwrap_or(false)
}
fn connect_timeout_secs(cfg: &newt_core::Config) -> u64 {
cfg.tui
.as_ref()
.map(|t| t.connect_timeout_secs)
.unwrap_or(5)
}
fn inference_timeout_secs(cfg: &newt_core::Config) -> u64 {
cfg.tui
.as_ref()
.map(|t| t.inference_timeout_secs)
.unwrap_or(120)
}
pub(crate) fn keep_alive_str(cfg: &newt_core::Config) -> String {
cfg.tui
.as_ref()
.map(|t| t.keep_alive.clone())
.unwrap_or_else(|| "5m".to_string())
}
fn summarizer_opts(
sum_cfg: &newt_core::SummarizerConfig,
cfg: &newt_core::Config,
num_ctx: Option<u32>,
color: bool,
) -> SummarizerOpts {
SummarizerOpts {
num_ctx,
keep_alive: sum_cfg
.keep_alive
.clone()
.unwrap_or_else(|| keep_alive_str(cfg)),
timeout_secs: sum_cfg.timeout_secs,
retries: sum_cfg.retries,
fallback_model: sum_cfg.fallback_model.clone(),
color,
run_allowance: None,
caps: newt_core::tty::LineCaps::detect(),
}
}
#[derive(Debug, PartialEq)]
enum SummarizerChoice {
Embedded(String),
DegradedSession,
}
fn default_summarizer_choice(embedded_gguf: Option<String>) -> SummarizerChoice {
match embedded_gguf {
Some(path) => SummarizerChoice::Embedded(path),
None => SummarizerChoice::DegradedSession,
}
}
fn embedded_summarizer_default() -> Option<String> {
#[cfg(feature = "embedded")]
{
newt_inference::palette::resolve_local(newt_inference::palette::default_model().name)
.map(|p| p.to_string_lossy().into_owned())
}
#[cfg(not(feature = "embedded"))]
{
None
}
}
fn warn_summarizer_once(flag: &std::sync::atomic::AtomicBool, msg: &str) {
if !flag.swap(true, std::sync::atomic::Ordering::Relaxed) {
eprintln!("{msg}");
}
}
fn resolve_summarizer_backend(
sum_cfg: &newt_core::SummarizerConfig,
inf_url: &str,
inf_model: &str,
inf_kind: newt_core::BackendKind,
inf_key: &Option<String>,
embedded_gguf: Option<String>,
) -> (
String,
String,
newt_core::BackendKind,
Option<String>,
Option<String>,
) {
let has_override = sum_cfg.kind.is_some()
|| sum_cfg.endpoint.is_some()
|| sum_cfg.model.is_some()
|| sum_cfg.model_path.is_some();
if !has_override {
match default_summarizer_choice(embedded_gguf) {
SummarizerChoice::Embedded(path) => {
let model = newt_inference::palette::default_model().name.to_string();
return (
String::new(),
model,
newt_core::BackendKind::Embedded,
None,
Some(path),
);
}
SummarizerChoice::DegradedSession => {
static WARNED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
warn_summarizer_once(&WARNED, "warning: the on-host embedded CPU summarizer is unavailable — context compaction will REUSE THE SESSION MODEL (on the GPU), which contends with the primary model under load and can stall the turn (see #979/#661). Enable it: `newt models pull`, and build with the `embedded` feature.");
}
}
}
let url = sum_cfg
.endpoint
.clone()
.unwrap_or_else(|| inf_url.to_string());
let model = sum_cfg
.model
.clone()
.unwrap_or_else(|| inf_model.to_string());
let kind = sum_cfg.kind.unwrap_or(inf_kind);
let model_path = sum_cfg.model_path.clone();
let key = if sum_cfg.endpoint.is_some() {
sum_cfg.resolve_api_key()
} else {
sum_cfg.resolve_api_key().or_else(|| inf_key.clone())
};
if has_override && !matches!(kind, newt_core::BackendKind::Embedded) {
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
warn_summarizer_once(&WARNED, "warning: using a summarizer OVERRIDE (session / off-box) instead of the on-host embedded CPU default — this can contend with the primary model under load (#661). Drop the `[summarizer]` backend override to use the embedded engine.");
}
(url, model, kind, key, model_path)
}
#[cfg(test)]
#[path = "lib_tests/summarizer_default_tests.rs"]
mod summarizer_default_tests;
fn summarizer_follows_session(
sum_cfg: &newt_core::SummarizerConfig,
embedded_gguf: Option<String>,
) -> bool {
let has_override = sum_cfg.kind.is_some()
|| sum_cfg.endpoint.is_some()
|| sum_cfg.model.is_some()
|| sum_cfg.model_path.is_some();
if !has_override {
return matches!(
default_summarizer_choice(embedded_gguf),
SummarizerChoice::DegradedSession
);
}
sum_cfg.endpoint.is_none()
&& sum_cfg.kind != Some(newt_core::BackendKind::Embedded)
&& sum_cfg.model_path.is_none()
}
#[allow(clippy::too_many_arguments)]
fn build_session_summarizer(
sum_cfg: &newt_core::SummarizerConfig,
cfg: &newt_core::Config,
inf_url: &str,
inf_model: &str,
inf_kind: newt_core::BackendKind,
inf_key: &Option<String>,
num_ctx: Option<u32>,
color: bool,
run_allowance: Option<std::sync::Arc<newt_core::agentic::run_allowance::RunAllowance>>,
) -> newt_core::Summarizer {
let (url, model, kind, key, model_path) = resolve_summarizer_backend(
sum_cfg,
inf_url,
inf_model,
inf_kind,
inf_key,
embedded_summarizer_default(),
);
let opts = SummarizerOpts {
run_allowance,
..summarizer_opts(sum_cfg, cfg, num_ctx, color)
};
let caps = opts.caps;
let summarizer = make_loop_summarizer(url, model, kind, key, model_path, opts);
with_summary_progress(summarizer, caps, color)
}
fn with_summary_progress(
summarizer: newt_core::Summarizer,
caps: newt_core::tty::LineCaps,
color: bool,
) -> newt_core::Summarizer {
Box::new(move |prompt| {
Box::pin(newt_core::tty::with_spinner(
caps,
"summarizing context…",
newt_core::tty::Sink::Stdout,
color,
summarizer(prompt),
))
})
}
#[cfg(test)]
#[path = "lib_tests/compaction_progress_tests.rs"]
mod compaction_progress_tests;
pub(crate) fn build_adjudicator(
cfg: &newt_core::Config,
inf_url: &str,
inf_model: &str,
inf_kind: newt_core::BackendKind,
inf_key: &Option<String>,
num_ctx: Option<u32>,
color: bool,
) -> newt_core::Summarizer {
let (url, model, kind, key) = cfg
.intake
.as_ref()
.and_then(|intake| intake.adjudicator.as_ref())
.map_or_else(
|| {
(
inf_url.to_string(),
inf_model.to_string(),
inf_kind,
inf_key.clone(),
)
},
|over| over.resolve(inf_url, inf_model, inf_kind, inf_key),
);
let opts = SummarizerOpts {
num_ctx,
color,
caps: newt_core::tty::LineCaps::detect(),
..SummarizerOpts::default()
};
make_loop_summarizer(url, model, kind, key, None, opts)
}
fn markdown_enabled(cfg: &newt_core::Config, color: bool) -> bool {
let mode = if newt_core::config::markdown_is_session_pinned() {
crate::migration_notices::read(newt_core::config::session_markdown_mode)
} else {
cfg.tui.as_ref().map(|t| t.markdown).unwrap_or_default()
};
mode.forced().unwrap_or(color) && color
}
fn context_manager(
cfg: &newt_core::Config,
session: Option<newt_core::ContextManager>,
) -> newt_core::ContextManager {
session
.or_else(|| cfg.context.as_ref().map(|c| c.manager))
.unwrap_or_default()
}
fn compaction_trigger_policy(cfg: &newt_core::Config) -> newt_core::CompactionTriggerPolicy {
if newt_core::config::compaction_trigger_is_session_pinned() {
return crate::migration_notices::read(
newt_core::config::session_compaction_trigger_policy,
);
}
configured_compaction_trigger_policy(cfg)
}
fn configured_compaction_trigger_policy(
cfg: &newt_core::Config,
) -> newt_core::CompactionTriggerPolicy {
cfg.context
.as_ref()
.map(|c| c.compaction_trigger_policy)
.unwrap_or_default()
}
fn compaction_trigger_policy_source(cfg: &newt_core::Config) -> &'static str {
if newt_core::config::compaction_trigger_is_session_pinned() {
"session"
} else if cfg.context.is_some() {
"config"
} else {
"default"
}
}
fn context_features(
cfg: &newt_core::Config,
manager: newt_core::ContextManager,
session: &newt_core::ContextFeatures,
kind: newt_core::BackendKind,
) -> newt_core::ContextFeatureSet {
let base = newt_core::ContextFeatureSet::base_for(manager, kind);
let with_config = cfg
.context
.as_ref()
.map(|c| c.features)
.unwrap_or_default()
.apply_to(base);
session.apply_to(with_config)
}
#[derive(Debug, Default, PartialEq, Eq)]
struct ContextCommandResult {
lines: Vec<String>,
set_manager: Option<newt_core::ContextManager>,
set_feature: Option<(newt_core::ContextFeature, bool)>,
set_budget: Option<u32>,
set_compaction_trigger_policy: Option<CompactionTriggerPolicyOverride>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CompactionTriggerPolicyOverride {
Set(newt_core::CompactionTriggerPolicy),
Reset,
}
fn handle_context_feature_arg(
arg: &str,
features: newt_core::ContextFeatureSet,
out: &mut ContextCommandResult,
) {
use newt_core::ContextFeature;
let mut parts = arg.split_whitespace();
let name = parts.next().unwrap_or("");
let toggle = parts.next();
match ContextFeature::from_keyword(name) {
Some(f) => match toggle {
None => {
let state = if features.get(f) { "on" } else { "off" };
let tail = if f.available() {
String::new()
} else {
format!(" (not yet available — #{})", f.issue())
};
out.lines
.push(format!("context feature {}: {state}{tail}", f.keyword()));
}
Some(t @ ("on" | "off")) => {
let want = t == "on";
if f.available() {
out.set_feature = Some((f, want));
out.lines
.push(format!("context feature {} → {t}", f.keyword()));
} else {
let state = if features.get(f) { "on" } else { "off" };
out.lines.push(format!(
"context feature '{}' is not yet available (see #{}) — staying {state}",
f.keyword(),
f.issue()
));
}
}
Some(other) => out.lines.push(format!(
"unknown toggle '{other}' for /context feature — use on|off"
)),
},
None => out.lines.push(format!(
"unknown context feature '{name}' — use {}",
ContextFeature::ALL
.iter()
.map(|f| f.keyword())
.collect::<Vec<_>>()
.join("|")
)),
}
}
fn handle_context_command(
rest: &str,
cfg: &newt_core::Config,
manager_override: Option<newt_core::ContextManager>,
feature_override: &newt_core::ContextFeatures,
kind: newt_core::BackendKind,
) -> ContextCommandResult {
use newt_core::{ContextFeature, ContextManager};
let rest = rest.trim();
let manager = context_manager(cfg, manager_override);
let features = context_features(cfg, manager, feature_override, kind);
let compaction_policy = compaction_trigger_policy(cfg);
let compaction_policy_source = compaction_trigger_policy_source(cfg);
let mgr_src = if manager_override.is_some() {
"session"
} else {
"config"
};
let feat_summary = || {
let on = features.enabled();
if on.is_empty() {
"none".to_string()
} else {
on.iter()
.map(|f| {
if f.available() {
f.keyword().to_string()
} else {
format!("{} (pending #{})", f.keyword(), f.issue())
}
})
.collect::<Vec<_>>()
.join(", ")
}
};
let mut out = ContextCommandResult::default();
if rest.is_empty() {
out.lines.push(format!(
"context manager: {} ({mgr_src}); features on: {}",
manager.keyword(),
feat_summary()
));
out.lines.push(format!(
"compaction trigger policy: {} ({compaction_policy_source})",
compaction_policy.keyword()
));
out.lines.push(
" /context manager <preset> · /context feature <name> [on|off] · \
/context compaction [headroom_aware|message_count|reset] · /context stats"
.to_string(),
);
} else if rest == "size" {
out.lines.push(
"context size: use /context size <N> to set the per-turn send \
budget (tokens), or /context size reset to restore the auto-sized value"
.to_string(),
);
} else if let Some(arg) = rest.strip_prefix("size ") {
let arg = arg.trim();
if arg == "reset" || arg == "auto" || arg == "0" {
out.set_budget = Some(0);
out.lines
.push("context size → reset (auto-sized budget)".to_string());
} else {
match arg.parse::<u32>() {
Ok(n) if n > 0 => {
out.set_budget = Some(n);
out.lines
.push(format!("context size → {n} tokens (session override)"));
}
_ => out.lines.push(format!(
"invalid context size '{arg}' — use a positive token count or 'reset'"
)),
}
}
} else if rest == "manager" {
out.lines.push(format!(
"context manager: {} ({mgr_src}) — \
use /context manager standard|append-only|progressive|distributed",
manager.keyword()
));
} else if let Some(name) = rest.strip_prefix("manager ") {
match ContextManager::from_keyword(name.trim()) {
Some(m) if m.available() => {
out.set_manager = Some(m);
out.lines.push(format!("context manager → {}", m.keyword()));
}
Some(m) => out.lines.push(format!(
"context manager '{}' is not yet available (see #546) — staying on {}",
m.keyword(),
manager.keyword()
)),
None => out.lines.push(format!(
"unknown context manager '{}' — use standard|append-only|progressive|distributed",
name.trim()
)),
}
} else if rest == "compaction" {
out.lines.push(format!(
"compaction trigger policy: {} ({compaction_policy_source}) — \
use /context compaction headroom_aware|message_count|reset",
compaction_policy.keyword()
));
} else if let Some(policy) = rest.strip_prefix("compaction ") {
let policy = policy.trim();
if policy.eq_ignore_ascii_case("reset") {
out.set_compaction_trigger_policy = Some(CompactionTriggerPolicyOverride::Reset);
let reset_policy = configured_compaction_trigger_policy(cfg);
let reset_source = if cfg.context.is_some() {
"config"
} else {
"default"
};
out.lines.push(format!(
"compaction trigger policy → {} ({reset_source})",
reset_policy.keyword()
));
} else if let Some(policy) = newt_core::CompactionTriggerPolicy::from_keyword(policy) {
out.set_compaction_trigger_policy = Some(CompactionTriggerPolicyOverride::Set(policy));
out.lines.push(format!(
"compaction trigger policy → {} (session override)",
policy.keyword()
));
} else {
out.lines.push(format!(
"unknown compaction trigger policy '{policy}' — \
use headroom_aware|message_count|reset"
));
}
} else if rest == "feature" || rest == "features" {
out.lines.push("context features:".to_string());
for f in ContextFeature::ALL {
let state = if features.get(f) { "on " } else { "off" };
let tail = if f.available() {
String::new()
} else {
format!(" (not yet available — #{})", f.issue())
};
out.lines.push(format!(" [{state}] {}{tail}", f.keyword()));
}
} else if let Some(arg) = rest.strip_prefix("feature ") {
handle_context_feature_arg(arg, features, &mut out);
} else if let Some(head) = rest.split_whitespace().next() {
if ContextFeature::from_keyword(head).is_some() {
handle_context_feature_arg(rest, features, &mut out);
} else {
out.lines.push(format!(
"unknown /context subcommand '{rest}' — \
use /context [manager <preset> | feature <name> [on|off] | \
compaction <policy> | size <N> | show | stats]"
));
}
} else {
out.lines.push(format!(
"unknown /context subcommand '{rest}' — \
use /context [manager <preset> | feature <name> [on|off] | \
compaction <policy> | size <N> | show | stats]"
));
}
out
}
#[allow(clippy::too_many_arguments)] fn context_stats_text(
gauge: Option<(u32, Option<u32>)>,
counters: &newt_core::CompressCounters,
compaction_policy: newt_core::CompactionTriggerPolicy,
compaction_policy_source: &str,
features: newt_core::ContextFeatureSet,
tool_offload_impact: Option<(u64, u64)>,
scratchpad_impact: Option<(u64, u64)>,
semantic_impact: Option<(u64, u64)>,
experiential_impact: Option<(u64, u64)>,
scheduled_impact: Option<(u64, u64)>,
) -> Vec<String> {
let mut lines = vec!["context stats".to_string()];
match gauge {
Some((used, Some(budget))) if budget > 0 => {
let pct = (u64::from(used) * 100 / u64::from(budget)) as u32;
lines.push(format!(
" budget: {} ({pct}% of the send window)",
newt_core::agentic::fmt_token_gauge(used, Some(budget))
));
}
Some((used, None)) => lines.push(format!(
" budget: {} (no context window known)",
newt_core::agentic::fmt_token_gauge(used, None)
)),
_ => lines.push(" budget: not yet measured (no completed turn)".to_string()),
}
lines.push(format!(
" automatic compaction: {} ({compaction_policy_source})",
compaction_policy.keyword()
));
for l in memory_compress_section(counters).lines() {
lines.push(l.to_string());
}
lines.push(" features:".to_string());
for f in newt_core::ContextFeature::ALL {
let state = if features.get(f) { "on " } else { "off" };
let tail = if f.available() {
String::new()
} else {
format!(" (pending #{})", f.issue())
};
let mut line = format!(" [{state}] {}{tail}", f.keyword());
if f == newt_core::ContextFeature::ToolOffload {
if let Some((spills, chars)) = tool_offload_impact {
line.push_str(&format!(
" — {spills} offloaded (~{}k chars elided)",
chars / 1000
));
}
}
if f == newt_core::ContextFeature::Scratchpad {
if let Some((keys, chars)) = scratchpad_impact {
line.push_str(&format!(" — {keys} keys (~{}k chars)", chars / 1000));
}
}
if f == newt_core::ContextFeature::Semantic {
if let Some((chunks, chars)) = semantic_impact {
line.push_str(&format!(
" — {chunks} chunks indexed (~{}k chars)",
chars / 1000
));
}
}
if f == newt_core::ContextFeature::Experiential {
if let Some((n, chars)) = experiential_impact {
line.push_str(&format!(" — {n} experiences (~{}k chars)", chars / 1000));
}
}
if f == newt_core::ContextFeature::Scheduled {
if let Some((steps, done)) = scheduled_impact {
line.push_str(&format!(" — {done}/{steps} plan steps done"));
}
}
lines.push(line);
}
lines
}
fn mid_loop_trim_threshold(cfg: &newt_core::Config) -> usize {
let threshold = cfg
.tui
.as_ref()
.map(|t| t.mid_loop_trim_threshold)
.unwrap_or_else(|| newt_core::TuiConfig::default().mid_loop_trim_threshold);
threshold.min(max_tool_rounds(cfg).saturating_sub(3))
}
fn build_check_cmd(cfg: &newt_core::Config) -> Option<String> {
cfg.tui.as_ref().and_then(|t| t.build_check_cmd.clone())
}
fn print_metrics(metrics: &newt_core::TurnMetrics, color: bool) {
let line = metrics.display_line();
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkGrey),
Print(format!(" {line}\n")),
ResetColor,
)
.ok();
} else {
println!(" {line}");
}
io::stdout().flush().ok();
}
fn print_thinking(color: bool) {
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkGrey),
Print(format!("{} thinking…", newt_core::tty::SPINNER_FRAMES[0])),
ResetColor,
)
.ok();
io::stdout().flush().ok();
}
}
fn erase_line() {
print!("\r\x1b[K");
io::stdout().flush().ok();
}
#[cfg(unix)]
fn is_lone_esc(bytes: &[u8]) -> bool {
bytes == [0x1b]
}
#[cfg(unix)]
fn is_ctrl_c(bytes: &[u8]) -> bool {
bytes.contains(&0x03)
}
pub(crate) trait SpillInput: Sync {
#[cfg(unix)]
fn scroll_up(&self) -> bool;
#[cfg(unix)]
fn scroll_down(&self) -> bool;
#[cfg(unix)]
fn toggle_expanded(&self) -> bool;
#[cfg(unix)]
fn expand_half(&self) -> bool;
#[cfg(unix)]
fn is_exploring(&self) -> bool;
#[cfg(unix)]
fn exit_explore(&self) -> bool;
#[cfg(unix)]
fn refresh_geometry(&self) -> bool;
#[cfg(all(unix, feature = "live-spill"))]
fn scroll_to_top(&self) -> bool;
#[cfg(all(unix, feature = "live-spill"))]
fn scroll_to_bottom(&self) -> bool;
#[cfg(all(unix, feature = "live-spill"))]
fn half_page_up(&self) -> bool;
#[cfg(all(unix, feature = "live-spill"))]
fn half_page_down(&self) -> bool;
}
#[cfg(feature = "live-spill")]
impl SpillInput for live_spill::LiveSpillRenderer {
#[cfg(unix)]
fn scroll_up(&self) -> bool {
self.scroll_up()
}
#[cfg(unix)]
fn scroll_down(&self) -> bool {
self.scroll_down()
}
#[cfg(unix)]
fn toggle_expanded(&self) -> bool {
self.toggle_expanded()
}
#[cfg(unix)]
fn expand_half(&self) -> bool {
self.expand_half()
}
#[cfg(unix)]
fn is_exploring(&self) -> bool {
self.is_exploring()
}
#[cfg(unix)]
fn exit_explore(&self) -> bool {
self.exit_explore()
}
#[cfg(unix)]
fn refresh_geometry(&self) -> bool {
self.refresh_geometry()
}
#[cfg(unix)]
fn scroll_to_top(&self) -> bool {
self.scroll_to_top()
}
#[cfg(unix)]
fn scroll_to_bottom(&self) -> bool {
self.scroll_to_bottom()
}
#[cfg(unix)]
fn half_page_up(&self) -> bool {
self.half_page_up()
}
#[cfg(unix)]
fn half_page_down(&self) -> bool {
self.half_page_down()
}
}
fn help_request(task: &str) -> Option<String> {
let body = task.trim_start_matches('/');
let mut parts = body.split_whitespace();
let cmd = parts.next().unwrap_or("");
let rest: Vec<&str> = parts.collect();
if cmd == "help" {
return rest.first().map(|c| c.to_string());
}
let is_help_token = |a: &&str| matches!(*a, "--help" | "-h" | "help");
if matches!(cmd, "start" | "rename" | "name") {
if rest.len() == 1 && is_help_token(&rest[0]) {
return Some(
if cmd == "start" {
"new"
} else {
"conversation"
}
.to_string(),
);
}
return None;
}
if cmd == "settings" {
if let Some(field) = rest
.first()
.and_then(|f| settings_form::Field::from_token(f))
{
if field.takes_free_text() {
if rest.len() == 2 && is_help_token(&rest[1]) {
return Some(cmd.to_string());
}
return None;
}
}
}
if rest.iter().any(is_help_token) {
return Some(cmd.to_string());
}
None
}
pub(crate) type SlashAsk<'a> = &'a dyn Fn(
&newt_core::interaction_surface::SurfaceInteraction,
) -> newt_core::HumanQuestionOutcome;
pub(crate) fn ask_on_this_terminal(
interaction: &newt_core::interaction_surface::SurfaceInteraction,
) -> newt_core::HumanQuestionOutcome {
let window =
newt_core::tty::Terminal::suspend_for_prompt(newt_core::tty::TerminalTaker::SlashForm);
crate::permissions::present_on_terminal(&window, interaction)
}
#[cfg(test)]
fn dispatch_slash(
input: &str,
workspace: &str,
color: bool,
verbose: bool,
markdown: bool,
) -> anyhow::Result<bool> {
dispatch_slash_with_ask(input, workspace, color, verbose, markdown, None)
}
fn dispatch_slash_with_ask(
input: &str,
workspace: &str,
color: bool,
verbose: bool,
markdown: bool,
ask: Option<SlashAsk<'_>>,
) -> anyhow::Result<bool> {
let (cmd, arg1, arg2) = slash_parts(input);
match cmd {
"exit" | "quit" | "help" | "version" | "workspace" | "config" => {
commands::meta::dispatch(cmd, arg1, workspace, color, verbose, markdown)
}
"prompt" | "vi" | "emacs" | "nano" | "edit-mode" | "thinking" | "nudge" | "tenacity"
| "cognition" | "psyche" => {
commands::settings::dispatch(cmd, arg1, input, workspace, color, verbose)
}
"models" | "probe" | "model" | "backend" | "backends" | "summarizer" | "dgx" => {
commands::model::dispatch(cmd, arg1, arg2, color, verbose)
}
"settings" => {
let fallback = ask_on_this_terminal;
let ask: SlashAsk<'_> = ask.unwrap_or(&fallback);
let rest = format!("{arg1} {arg2}");
for line in settings_form::run(ask, rest.trim()) {
print_newt(&line, color, verbose);
}
Ok(true)
}
"crew" => {
let fallback = ask_on_this_terminal;
commands::crew::dispatch(arg1, arg2, color, verbose, ask.unwrap_or(&fallback))
}
"setup" => commands::setup::dispatch(arg1, color, verbose),
"discuss" | "chat" => {
print_newt(
"there is no pending decision to discuss right now",
color,
verbose,
);
Ok(true)
}
other => {
print_newt(&slash_registry::fallthrough_message(other), color, verbose);
Ok(true)
}
}
}
fn slash_parts(input: &str) -> (&str, &str, &str) {
let body = input.trim_start_matches('/').trim_start();
let (cmd, rest) = body.split_once(char::is_whitespace).unwrap_or((body, ""));
let rest = rest.trim_start();
let (arg1, arg2) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
(cmd, arg1, arg2.trim())
}
pub(crate) fn fetch_models_for(
url: &str,
kind: newt_core::BackendKind,
api_key: Option<&str>,
) -> anyhow::Result<Vec<String>> {
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
newt_core::backend_probe::api_for(kind)
.list_models(&client, url, api_key)
.await
})
})
}
pub(crate) fn run_newt_subcmd(args: &[&str], color: bool, verbose: bool) -> anyhow::Result<()> {
let exe = std::env::current_exe()?;
let status = std::process::Command::new(&exe).args(args).status()?;
if !status.success() {
print_newt(
&format!("command exited with status {}", status.code().unwrap_or(-1)),
color,
verbose,
);
}
Ok(())
}
fn bang_command(input: &str) -> Option<&str> {
let rest = input.strip_prefix('!')?.trim();
if rest.is_empty() {
None
} else {
Some(rest)
}
}
fn bang_shell() -> (String, &'static str) {
#[cfg(windows)]
{
let sh = std::env::var("COMSPEC").unwrap_or_else(|_| "cmd".to_string());
(sh, "/C")
}
#[cfg(not(windows))]
{
let sh = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
(sh, "-c")
}
}
fn run_bang_escape(cmd: &str, color: bool, verbose: bool) {
let (command, shell) = bang_shell_command(cmd);
#[cfg(unix)]
{
run_bang_escape_unix(command, &shell, color, verbose);
}
#[cfg(not(unix))]
{
let mut command = command;
match command.status() {
Ok(status) if status.success() => {}
Ok(status) => print_newt(
&format!("exit {}", status.code().unwrap_or(-1)),
color,
verbose,
),
Err(e) => print_newt(&format!("! failed to run `{shell}`: {e}"), color, verbose),
}
}
}
#[derive(Debug)]
pub(crate) enum OperatorCommand {
Shell(String),
Argv(Vec<String>),
}
impl OperatorCommand {
#[cfg(all(unix, feature = "rich-tui"))]
fn prepare(&self) -> io::Result<(std::process::Command, String)> {
match self {
Self::Shell(command) => Ok(bang_shell_command(command)),
Self::Argv(argv) => Ok((operator_argv_command(argv)?, argv[0].clone())),
}
}
}
fn operator_argv_command(argv: &[String]) -> io::Result<std::process::Command> {
let program = argv
.first()
.filter(|program| !program.trim().is_empty())
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "login argv requires a program")
})?;
let mut command = std::process::Command::new(program);
command.args(&argv[1..]);
Ok(command)
}
fn run_login_argv(argv: &[String], color: bool, verbose: bool) -> anyhow::Result<bool> {
let command = operator_argv_command(argv)?;
#[cfg(unix)]
{
Ok(run_bang_escape_unix(command, &argv[0], color, verbose))
}
#[cfg(not(unix))]
{
let _ = (color, verbose);
let mut command = command;
Ok(command.status()?.success())
}
}
fn bang_shell_command(cmd: &str) -> (std::process::Command, String) {
let (shell, flag) = bang_shell();
let mut command = std::process::Command::new(&shell);
command.arg(flag).arg(cmd);
(command, shell)
}
#[cfg(unix)]
fn run_bang_escape_unix(
mut command: std::process::Command,
shell: &str,
color: bool,
verbose: bool,
) -> bool {
use std::os::unix::process::CommandExt as _;
let tty = libc::STDIN_FILENO;
let foreground = unsafe { libc::tcgetpgrp(tty) };
if foreground >= 0 {
unsafe {
command.pre_exec(move || {
if libc::setpgid(0, 0) != 0 {
return Err(io::Error::last_os_error());
}
let old = libc::signal(libc::SIGTTOU, libc::SIG_IGN);
let changed = libc::tcsetpgrp(tty, libc::getpgrp());
let error = (changed != 0).then(io::Error::last_os_error);
libc::signal(libc::SIGTTOU, old);
error.map_or(Ok(()), Err)
});
}
}
let status = {
let _foreground = RestoreOnDrop {
restore: || {
if foreground >= 0 {
unsafe {
let old = libc::signal(libc::SIGTTOU, libc::SIG_IGN);
libc::tcsetpgrp(tty, foreground);
libc::signal(libc::SIGTTOU, old);
}
}
},
};
command.status()
};
match status {
Ok(status) if status.success() => true,
Ok(status) => {
let msg = match status.code() {
Some(code) => format!("exit {code}"),
None => "interrupted".to_string(),
};
print_newt(&msg, color, verbose);
false
}
Err(e) => {
print_newt(&format!("! failed to run `{shell}`: {e}"), color, verbose);
false
}
}
}
fn cd_command(input: &str) -> Option<&str> {
let trimmed = input.trim();
if !trimmed.starts_with('/') {
return None;
}
let slash_verb = trimmed.trim_start_matches('/');
let (verb, arg) = slash_verb
.split_once(char::is_whitespace)
.map_or((slash_verb, ""), |(v, a)| (v, a.trim()));
(verb == "cd").then_some(arg)
}
fn lexical_normalize(p: &std::path::Path) -> std::path::PathBuf {
let mut out = std::path::PathBuf::new();
for comp in p.components() {
match comp {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
out.pop();
}
c => out.push(c.as_os_str()),
}
}
out
}
fn confine_under_root(
root: &std::path::Path,
session_cwd: &std::path::Path,
arg: &str,
) -> Option<std::path::PathBuf> {
let joined = if arg.is_empty() {
session_cwd.to_path_buf()
} else {
let p = std::path::Path::new(arg);
if p.is_absolute() {
p.to_path_buf()
} else {
session_cwd.join(p)
}
};
let normalized = lexical_normalize(&joined);
let root_norm = lexical_normalize(root);
normalized.starts_with(&root_norm).then_some(normalized)
}
fn run_cd(arg: &str, session_cwd: &mut std::path::PathBuf, root: &str, color: bool, verbose: bool) {
if matches!(arg, "--help" | "-h") {
print_newt(
"/cd [dir] — change the session working dir (below the start dir); bare /cd returns to the root",
color,
verbose,
);
return;
}
let root_path = std::path::Path::new(root);
if arg.is_empty() {
*session_cwd = lexical_normalize(root_path);
return;
}
match confine_under_root(root_path, session_cwd, arg) {
Some(t) if t.is_dir() => *session_cwd = t,
Some(t) => print_newt(
&format!("cd: not a directory: {}", t.display()),
color,
verbose,
),
None => print_newt(
"cd: outside the session root (can't climb above it)",
color,
verbose,
),
}
}
#[cfg(test)]
#[path = "lib_tests/cd_tests.rs"]
mod cd_tests;
fn resolve_workspace(path: Option<&std::path::Path>) -> String {
path.map(|p| p.to_string_lossy().into_owned())
.or_else(|| {
std::env::current_dir()
.ok()
.map(|d| d.to_string_lossy().into_owned())
})
.unwrap_or_else(|| "(unknown)".into())
}
#[cfg(test)]
fn test_persona(name: &str, prompt: &str, path: std::path::PathBuf) -> Persona {
Persona {
name: name.to_string(),
prompt: prompt.to_string(),
path,
profile: newt_core::RoleProfile {
prompt: prompt.to_string(),
..Default::default()
},
}
}
#[cfg(test)]
#[path = "lib_tests/core.rs"]
mod tests;
#[cfg(test)]
#[path = "lib_tests/run_command_confinement_tests.rs"]
mod run_command_confinement_tests;
#[cfg(test)]
#[path = "lib_tests/disable_ocap_session_tests.rs"]
mod disable_ocap_session_tests;
#[cfg(test)]
#[path = "lib_tests/ocap_propose_tests.rs"]
mod ocap_propose_tests;
#[cfg(test)]
#[path = "lib_tests/note_sink_wiring_tests.rs"]
mod note_sink_wiring_tests;
#[cfg(test)]
#[path = "lib_tests/close_extraction_tests.rs"]
mod close_extraction_tests;
#[cfg(test)]
#[path = "lib_tests/skills_integration.rs"]
mod skills_integration_tests;
#[cfg(test)]
#[path = "lib_tests/operating_mode_tests.rs"]
mod operating_mode_tests;
#[cfg(test)]
#[path = "lib_tests/plan_draft_state_tests.rs"]
mod plan_draft_state_tests;
#[cfg(test)]
#[path = "lib_tests/posture_command_tests.rs"]
mod posture_command_tests;
#[cfg(test)]
#[path = "lib_tests/tool_round_cap_tests.rs"]
mod tool_round_cap_tests;
#[cfg(test)]
#[path = "lib_tests/terminal_probe_tests.rs"]
mod terminal_probe_tests;
#[cfg(all(test, unix))]
#[path = "lib_tests/fd_exhaustion_tests.rs"]
mod fd_exhaustion_tests;
#[cfg(test)]
#[path = "lib_tests/helper_fns.rs"]
mod helper_fn_tests;
#[cfg(test)]
#[path = "lib_tests/tenacity_indicator_tests.rs"]
mod tenacity_indicator_tests;
#[cfg(test)]
#[path = "lib_tests/persona_helper_tests.rs"]
mod persona_helper_tests;
#[cfg(test)]
#[path = "lib_tests/env_resolution.rs"]
mod env_resolution_tests;
#[cfg(test)]
#[path = "lib_tests/http_loop.rs"]
mod http_loop_tests;
#[cfg(feature = "native-permission-test")]
#[doc(hidden)]
pub mod native_permission_test;