Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/agent/idle_compaction_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ impl SessionParts {
connectivity: aither_connectivity::Connectivity::manual(true).0,
rules_import: self.rules_import.clone(),
limits: crate::agent::LiveLimits::of(&MayConfig::default()),
services: crate::terminal_commands::SharedServices::new(&self.dir.join(".may")),
model_context_window: None,
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/agent/may.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ pub struct MayAgentConfig {
pub rules_import: Option<crate::rules_import::RulesImport>,
/// The daemon's live limits, shared by every agent it builds.
pub limits: super::LiveLimits,
/// The repo-search handle and PR land locks the daemon shares.
pub services: crate::terminal_commands::SharedServices,
}

/// Session-scoped inputs for [`MayAgent::from_config_with_model_and_permissions`].
Expand Down Expand Up @@ -377,6 +379,8 @@ pub struct MayAgentSessionInput<'a> {
pub rules_import: Option<crate::rules_import::RulesImport>,
/// The daemon's live limits, shared by every agent it builds.
pub limits: super::LiveLimits,
/// The repo-search handle and PR land locks the daemon shares.
pub services: crate::terminal_commands::SharedServices,
/// The model's context window as the daemon resolved it (provider
/// report, then the model table); the agent compacts against it.
pub model_context_window: Option<u32>,
Expand Down Expand Up @@ -970,6 +974,7 @@ impl MayAgent {
#[cfg(target_os = "macos")]
computer: config.computer.clone(),
rules_import: config.rules_import.clone(),
services: config.services.clone(),
},
))
.await?;
Expand Down Expand Up @@ -1266,6 +1271,7 @@ impl MayAgent {
connectivity,
rules_import,
limits,
services,
model_context_window,
} = input;
let selector = super::provider::parse_model_selector(model)?;
Expand Down Expand Up @@ -1372,6 +1378,7 @@ impl MayAgent {
db,
rules_import,
limits,
services,
})
}

Expand Down
4 changes: 3 additions & 1 deletion src/agent/pr_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ mod tests {
use aither_core::llm::ToolContext;

use super::{CommandRole, PrArgs, PrFilter, PrStore, PrTool, Tool};
use crate::pr::PrContext;
use crate::pr::{LandLocks, PrContext};
use crate::repo_clone::test_sandbox::TestTerminal;

fn text(result: &aither_core::llm::ToolResult) -> String {
Expand Down Expand Up @@ -291,6 +291,7 @@ mod tests {
"s1",
),
github_api: PrContext::default_github_api(),
land_locks: LandLocks::default(),
task_events: None,
ci: None,
publish: None,
Expand Down Expand Up @@ -398,6 +399,7 @@ mod tests {
screen: None,
signing: user.signing(&tmp.join("may-home"), "s1"),
github_api: PrContext::default_github_api(),
land_locks: LandLocks::default(),
task_events: None,
ci: None,
publish: None,
Expand Down
15 changes: 10 additions & 5 deletions src/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use std::path::Path;

use crate::secrets::Secrets;
use crate::secrets::{SecretPatterns, Secrets};

/// Masked result of a bash-mode command, ready for display and the agent.
#[derive(Debug, serde::Serialize)]
Expand Down Expand Up @@ -47,8 +47,13 @@ fn cap_stream(bytes: &[u8]) -> String {
/// keychain secrets injected as environment variables. Output is masked
/// before it returns.
#[must_use]
pub fn run_bash_command(command: &str, cwd: &Path, secrets: &Secrets) -> BashOutcome {
let (_, command_secret) = secrets.mask(command);
pub fn run_bash_command(
command: &str,
cwd: &Path,
secrets: &Secrets,
patterns: &SecretPatterns,
) -> BashOutcome {
let (_, command_secret) = secrets.mask(patterns, command);
let spawn = || {
std::process::Command::new("bash")
.arg("-c")
Expand All @@ -68,8 +73,8 @@ pub fn run_bash_command(command: &str, cwd: &Path, secrets: &Secrets) -> BashOut
error: Some(format!("failed to run bash: {error}")),
},
Ok(output) => {
let (stdout, out_secret) = secrets.mask(&cap_stream(&output.stdout));
let (stderr, err_secret) = secrets.mask(&cap_stream(&output.stderr));
let (stdout, out_secret) = secrets.mask(patterns, &cap_stream(&output.stdout));
let (stderr, err_secret) = secrets.mask(patterns, &cap_stream(&output.stderr));
// `had_secret` tracks output only — a command-side hit already
// warned at submit time.
BashOutcome {
Expand Down
1 change: 1 addition & 0 deletions src/bin/may/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ where
#[cfg(target_os = "macos")]
computer: None,
rules_import: None,
services: may_agent::SharedServices::new(&context.may_dir),
llm,
ask_flagship: None,
ask_balanced: None,
Expand Down
7 changes: 6 additions & 1 deletion src/bin/may/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,7 @@ where

tracing::info!("=== LOOP START ===");
let mut phases = may_agent::tui::perf::LoopPhases::default();
let jank_threshold = may_agent::tui::perf::jank_threshold();

// First draw: updates content_height/view_height for accurate overflow calculation.
if needs_draw {
Expand Down Expand Up @@ -2288,9 +2289,12 @@ where
}) => {
let tx = events.sender();
let cwd = app.cwd.clone();
let patterns = app.secret_patterns.clone();
spawn(async move {
let mut outcome = blocking::unblock(move || {
may_agent::bash::run_bash_command(&command, &cwd, &secrets)
may_agent::bash::run_bash_command(
&command, &cwd, &secrets, &patterns,
)
})
.await;
outcome.call_id = call_id;
Expand Down Expand Up @@ -2331,6 +2335,7 @@ where
// Report a stall whenever one busy iteration exceeded the jank
// threshold; the log carries the render breakdown for repro.
may_agent::tui::perf::report(
jank_threshold,
&phases,
&app.frame_stats,
app.scroll,
Expand Down
9 changes: 7 additions & 2 deletions src/crash_signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ const PATH_CAP: usize = 1024;

/// What a signal handler needs, reachable without parameters. A handler
/// receives only the signal and its `siginfo`, so the state it uses has to
/// live in a static — the one place a static is unavoidable.
/// live in a static — the one place a static is unavoidable. `sigaction`
/// offers no user-data argument and a handler cannot be a closure, so no
/// handler-safe alternative to a static exists.
struct CrashState {
ring: LogRingHandle,
bugs_dir: CString,
Expand All @@ -37,7 +39,10 @@ struct CrashState {
static CRASH_STATE: OnceLock<CrashState> = OnceLock::new();

/// Set once the first dump is written so a nested or chained signal (the
/// overflow that ends in abort) never writes a second file.
/// overflow that ends in abort) never writes a second file. A static for the
/// same reason as [`CRASH_STATE`]: the handler installed with `sigaction`
/// (`SA_SIGINFO`) receives no context pointer, and an atomic is the one
/// async-signal-safe way to share this flag between invocations.
static DUMPED: AtomicBool = AtomicBool::new(false);

/// Install the fatal-signal log dump for `SIGSEGV`, `SIGBUS` and `SIGABRT`.
Expand Down
18 changes: 12 additions & 6 deletions src/daemon/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ use crate::agent::MayAgent;
use crate::ask_user;
use crate::db::RunFence;
use crate::prompt_mentions;
use crate::quota;
use crate::session::{
AcquireError, CancelReason, ChatHistory, ChatMessage, ChatMessageStats, FinishError,
MayHomePaths, MessageRole, PendingMessageState, PendingUserMessage, PersistedAttachment,
Expand Down Expand Up @@ -1860,11 +1859,13 @@ pub(super) async fn run_bash(
)
.map_err(internal_error)?;
let run_secrets = secrets.clone();
let outcome =
blocking::unblock(move || crate::bash::run_bash_command(&command, &cwd, &run_secrets))
.await;
let patterns = state.secret_patterns.clone();
let outcome = blocking::unblock(move || {
crate::bash::run_bash_command(&command, &cwd, &run_secrets, &patterns)
})
.await;
// The transcript returned to the client never carries the raw secret.
let (masked_command, _) = secrets.mask(&outcome.command);
let (masked_command, _) = secrets.mask(&state.secret_patterns, &outcome.command);
Ok(Json(crate::daemon::types::BashResponse {
command: masked_command,
stdout: outcome.stdout,
Expand Down Expand Up @@ -2529,7 +2530,11 @@ fn run_agent_loop_attempts<'a>(
.config_snapshot()
.preferences
.auto_continue_on_quota_reset
.then(|| quota::reset_at(&err_msg, jiff::Timestamp::now()))
.then(|| {
state
.quota_patterns
.reset_at(&err_msg, jiff::Timestamp::now())
})
.flatten()
.map(RetryWait::QuotaReset),
};
Expand Down Expand Up @@ -4370,6 +4375,7 @@ fn build_session_agent<'a>(
connectivity: state.connectivity.clone(),
rules_import: ctx.rules_import.clone(),
limits: state.live_limits.clone(),
services: state.services.clone(),
model_context_window,
},
)
Expand Down
1 change: 1 addition & 0 deletions src/daemon/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ async fn index_coverage(
) -> Result<Option<crate::client::IndexCoverage>, String> {
let status = async {
let client = state
.services
.repo_search
.client()
.await
Expand Down
4 changes: 2 additions & 2 deletions src/daemon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ use anyhow::Context as _;
use skyzen::routing::Router;

use crate::config::MayConfig;
use crate::repo_search::RepoSearchCollections;
use crate::repo_search::auto_index::AutoIndexer;
use crate::repo_search::indexer::{IndexerStatus, SearchIndexer};
use crate::repo_search::{RepoSearch, RepoSearchCollections};
use crate::ui_event_bus::UiEventTopics;

/// How long a shutdown waits for running turns before exiting anyway.
Expand Down Expand Up @@ -508,7 +508,7 @@ pub async fn run_daemon(options: DaemonOptions) -> anyhow::Result<()> {
}
});

let repo_search = RepoSearch::global(paths.root());
let repo_search = state.services.repo_search.clone();
let indexer = SearchIndexer::new(
state.db.clone(),
RepoSearchCollections::new(repo_search.clone()),
Expand Down
4 changes: 3 additions & 1 deletion src/daemon/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ pub async fn start(
tool_distribution,
tool_environments: crate::agent::ToolEnvironmentCache::spawn(),
job_server,
repo_search: crate::repo_search::RepoSearch::global(&may_dir),
services: crate::terminal_commands::SharedServices::new(&may_dir),
quota_patterns: crate::quota::QuotaPatterns::default(),
secret_patterns: crate::secrets::SecretPatterns::default(),
may_dir,
search_index_wake_tx,
search_index_wake,
Expand Down
12 changes: 9 additions & 3 deletions src/daemon/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,12 @@ pub struct DaemonState {
pub(crate) search_index_wake: async_channel::Receiver<()>,
/// Latest outcome of the search indexer.
pub(crate) search_index_status: crate::repo_search::indexer::IndexerStatus,
/// The repo-search daemon handle worktree coverage is polled through.
pub(crate) repo_search: crate::repo_search::RepoSearch,
/// The compiled quota-reset matchers turns parse upstream errors with.
pub(crate) quota_patterns: crate::quota::QuotaPatterns,
/// The compiled secret shapes bash-mode output is masked with.
pub(crate) secret_patterns: crate::secrets::SecretPatterns,
/// The repo-search handle and PR land locks every session's agent shares.
pub(crate) services: crate::terminal_commands::SharedServices,
/// Signalled by `POST /api/daemon/shutdown`.
pub(crate) daemon_shutdown: Arc<tokio::sync::Notify>,
/// Set once shutdown starts draining: new turns are refused so the
Expand Down Expand Up @@ -1430,7 +1434,9 @@ impl DaemonState {
.await
.expect("test jobserver starts"),
may_dir: may_dir.to_path_buf(),
repo_search: crate::repo_search::RepoSearch::global(may_dir),
services: crate::terminal_commands::SharedServices::new(may_dir),
quota_patterns: crate::quota::QuotaPatterns::default(),
secret_patterns: crate::secrets::SecretPatterns::default(),
search_index_wake_tx,
search_index_wake,
search_index_status: crate::repo_search::indexer::IndexerStatus::default(),
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ pub use session::{
};
pub use skill::{SkillInfo, SkillManager, SkillSource};
pub use terminal_commands::{
SharedTerminalCommandConfig, TerminalCommandsError, register_shared_terminal_commands,
SharedServices, SharedTerminalCommandConfig, TerminalCommandsError,
register_shared_terminal_commands,
};
pub use terminal_permission_handler::TerminalPermissionHandler;
pub use workspace::{Workspace, WorkspaceConfig};
Expand Down
36 changes: 24 additions & 12 deletions src/pr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
use std::sync::{Arc, Mutex};

use askama::Template;

Expand Down Expand Up @@ -104,6 +104,8 @@ pub struct PrContext {
pub signing: crate::commit_signing::CommitSigning,
/// GitHub REST client the GitHub backend's direct calls go through.
pub github_api: GitHubRest<PrTokenSource>,
/// The per-repository land locks every session of the daemon shares.
pub land_locks: LandLocks,
/// The main agent's `tasks.md` inbox: told when a PR opens, merges, is
/// rejected or closes.
pub task_events: Option<crate::harness::working_document::WorkEvents>,
Expand Down Expand Up @@ -243,14 +245,20 @@ impl merge_watch::ChecksObserver for CiObserver<'_> {
}

/// Serialized per-repo merge: one in-flight land per repository.
static LAND_LOCKS: LazyLock<Mutex<HashMap<String, Arc<Mutex<()>>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));

fn land_lock(repo_key: &str) -> Arc<Mutex<()>> {
let mut locks = LAND_LOCKS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
locks.entry(repo_key.to_string()).or_default().clone()
///
/// Cloning shares the locks, so the daemon hands one set to every session and
/// a land in one session excludes a land of the same repository in another.
#[derive(Clone, Debug, Default)]
pub struct LandLocks(Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>);

impl LandLocks {
fn get(&self, repo_key: &str) -> Arc<Mutex<()>> {
let mut locks = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
locks.entry(repo_key.to_string()).or_default().clone()
}
}

/// The resolved clone a `pr` operates on.
Expand Down Expand Up @@ -604,7 +612,7 @@ impl PrStore {
return Err(PrError::CheckFailed(err));
}
// Serialized land: one in-flight merge per repository.
let lock = land_lock(&resolved.record.repo_key);
let lock = self.context.land_locks.get(&resolved.record.repo_key);
self.event(&pr, "merging", None).await;
let result = blocking::unblock({
let checkout = resolved.record.source.clone();
Expand Down Expand Up @@ -1064,7 +1072,7 @@ impl PrStore {
let external = pr
.external_number
.ok_or_else(|| PrError::State("github PR missing its number".to_string()))?;
let lock = land_lock(&resolved.record.repo_key);
let lock = self.context.land_locks.get(&resolved.record.repo_key);
let github = merge_watch::GithubPr {
api: &self.context.github_api,
owner_repo: &owner_repo,
Expand Down Expand Up @@ -2056,7 +2064,7 @@ fn git(dir: Option<&Path>, args: &[&str]) -> Result<String, crate::repo_clone::R
#[cfg(test)]
mod tests {
use super::{
BranchState, GitHubRest, PrContext, PrStore, PrTokenSource, git, github_branch,
BranchState, GitHubRest, LandLocks, PrContext, PrStore, PrTokenSource, git, github_branch,
github_owner_repo, host_git, push_pinned,
};
use crate::commit_signing::test_user::TestUser;
Expand Down Expand Up @@ -2179,6 +2187,7 @@ mod tests {
screen: None,
signing: self.signing(id),
github_api: PrContext::default_github_api(),
land_locks: LandLocks::default(),
task_events: None,
ci: None,
publish: None,
Expand Down Expand Up @@ -2244,6 +2253,7 @@ mod tests {
PrTokenSource::Fixed("test-token".to_string()),
base,
),
land_locks: LandLocks::default(),
task_events: None,
ci: None,
publish: None,
Expand Down Expand Up @@ -2703,6 +2713,7 @@ mod tests {
screen,
signing: fx.signing(id),
github_api,
land_locks: LandLocks::default(),
task_events: None,
ci: None,
publish: None,
Expand Down Expand Up @@ -2954,6 +2965,7 @@ mod tests {
screen: None,
signing: fx.signing(id),
github_api: PrContext::default_github_api(),
land_locks: LandLocks::default(),
task_events: None,
ci: None,
publish: None,
Expand Down
Loading
Loading