Watch
0
0
Fork
You've already forked hyperhive
0

hive-runtime: record a new ACP session only once its first prompt is answered

The session id was written to the session file right after session/new,
but the system prompt rides on the first prompt only. If that prompt
failed, the next turn (or the next harness start) resumed the recorded
session as not-new and the system prompt never reached it.

The id is now written after the first session/prompt gets its reply.
A failed first prompt leaves nothing recorded, so the next turn starts
a new session and sends the system prompt again. Chosen over a separate
"system prompt delivered" marker: one file, and "recorded" already means
"usable".

Tests drive AcpRuntime against a scripted sh agent that fails the first
prompt: in-process and across a restart, the retry is a new session
carrying the system prompt.

Also: PermissionPolicy now sees a PermissionAsk (kind plus the MCP
server the tool belongs to, matched by name against the servers handed
to the session), so a caller can tell MCP tool calls from other `other`
requests.

Refs #4391
This commit is contained in:
atlas 2026-09-29 21:47:28 +02:00 • committed by mara
commit a2ab40cc69
6 changed files with 240 additions and 61 deletions

View file

@ -33,10 +33,6 @@ impl StreamMapper {
/// `servers` are the MCP server names handed to the agent, used to give
/// their tools claude's `mcp__<server>__<tool>` names.
pub(super) fn new(servers: Vec<String>) -> Self {
let mut servers = servers;
// Longest first, so a server whose name prefixes another's never
// claims the other's tools.
servers.sort_by_key(|s| std::cmp::Reverse(s.len()));
Self {
servers,
text: String::new(),
@ -226,18 +222,30 @@ pub(super) fn canonical_tool_name(name: &str, servers: &[String]) -> String {
if name.starts_with("mcp__") {
return name.to_owned();
}
for server in servers {
if let Some(rest) = name
.strip_prefix(server.as_str())
.and_then(|r| r.strip_prefix('_'))
{
split_mcp_name(name, servers).map_or_else(
|| name.to_owned(),
|(server, tool)| format!("mcp__{server}__{tool}"),
)
}
/// Which of `servers` a tool named `<server>_<tool>`, `<server>__<tool>` or
/// `mcp__<server>__<tool>` belongs to, if any.
pub(super) fn mcp_server_of<'a>(name: &str, servers: &'a [String]) -> Option<&'a str> {
split_mcp_name(name, servers).map(|(server, _)| server)
}
/// `(server, tool)` for an MCP tool name, matching the longest server name so
/// one that prefixes another's never claims the other's tools.
fn split_mcp_name<'a, 'n>(name: &'n str, servers: &'a [String]) -> Option<(&'a str, &'n str)> {
let bare = name.strip_prefix("mcp__").unwrap_or(name);
servers
.iter()
.filter_map(|server| {
let rest = bare.strip_prefix(server.as_str())?.strip_prefix('_')?;
let tool = rest.strip_prefix('_').unwrap_or(rest);
if !tool.is_empty() {
return format!("mcp__{server}__{tool}");
}
}
}
name.to_owned()
(!tool.is_empty()).then_some((server.as_str(), tool))
})
.max_by_key(|(server, _)| server.len())
}
fn chunk_text(update: &Value) -> &str {