Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0a32df1
apply_patch: Codex's patch tool beside edit, freeform on Responses
claude Oct 28, 2026
314409b
apply_patch: end-of-file insertions sort, moves keep their mode, cut …
claude Oct 28, 2026
a179357
apply_patch: schema 56 after declared answers took 55
claude Oct 28, 2026
c5e5161
apply_patch: each file once, a move to itself is an update, staging b…
claude Oct 28, 2026
fbfe174
apply_patch: destinations checked before writing, a lone empty line k…
claude Oct 28, 2026
b8c0013
apply_patch: a patch's paths deduplicated through a set
claude Oct 28, 2026
4029c0b
apply_patch: a cut patch's started event names at most 64 KiB of paths
claude Oct 28, 2026
030be8a
Sonny: a patch line with no change mark draws as context
claude Oct 28, 2026
5ccc5c3
apply_patch: runs in a bot's sandbox, as write and edit do
claude Oct 28, 2026
179856f
apply_patch: a process slot only while the helper writes; Sonny reads…
claude Oct 28, 2026
e87bb85
approver: a custom tool's call and result show in the judge's transcript
claude Oct 28, 2026
d8f3061
apply_patch: a patch can't write both a file and a file below it
claude Oct 28, 2026
0291200
approver: a planned patch's files are read before its contents are cl…
claude Oct 28, 2026
1150a49
Sonny: placing a patch's chunk checks its last line first, within a b…
claude Oct 28, 2026
372eda8
apply_patch: matching reads lines in place and stops at a bound on li…
claude Oct 28, 2026
687450c
apply_patch: nothing to match is found at the start; nested paths che…
claude Oct 28, 2026
af05f6b
apply_patch: a file's punctuation-plain lines are packed into one string
claude Oct 28, 2026
4b40f42
Sonny: a patch's file is split into lines once, and its chunks share …
claude Oct 28, 2026
cbdc8dc
Calls in a row that only read or run commands run together, up to fou…
claude Oct 28, 2026
78c5a4d
Merge the merged apply_patch branch, keeping main's squash of it
claude Oct 28, 2026
e32bf35
A batch's results share commits; stored reads stay sequential; record…
claude Oct 28, 2026
0c49042
A batch runs at most half the process budget at once; its results wai…
claude Oct 28, 2026
008da79
A batch drops each committed result's acknowledgement as it comes
claude Oct 28, 2026
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
25 changes: 25 additions & 0 deletions docs/DAEMON_MEASUREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5644,3 +5644,28 @@ Hot-path screen, in the same alternating run: the previous pacer change against
Every range overlaps the other build's. The medians for completion p99 and
lifecycle CPU are a little higher; neither bench waits on a learned limit,
so the new path costs one look at an empty list of bookings per call.

## Calls in a row run together (#291)

The Perf workflow's matched screen, base main 93e57b8 against the PR head
78c5a4d, on the GitHub runner (2026-10-28):

| Bench | Measure | Base | Head | Change |
| --- | --- | ---: | ---: | ---: |
| admission | lone submit CPU, ms | 38.914 | 38.556 | -0.9% |
| admission | burst submit p99, ms | 6.025 | 5.86 | -2.7% |
| completion | release to terminal p99, ms | 13.442 | 13.297 | -1.1% |
| footprint | idle RSS, MiB | 17.418 | 17.51 | +0.5% |
| footprint | per idle bot, KiB | 1.578 | 1.61 | +2.0% |
| footprint | RSS after a turn each, MiB | 26.357 | 26.639 | +1.1% |
| footprint | CPU per turn, ms | 1.284 | 1.268 | -1.2% |
| lifecycle | CPU, s | 1.225 | 1.165 | -4.9% |
| lifecycle | p95 turn, ms | 583.266 | 581.296 | -0.3% |
| lifecycle | peak RSS, MiB | 23.912 | 24.0 | +0.4% |

Store jobs per turn did not change. Every change is within the gate's 15%.
These benches make one call per round, so they show the lone path unchanged.
The batch path's gain is wall time: in `tests/test_wait.py`, two commands
that each sleep 0.6 s finish in under 1.1 s. Results of a batch are queued
to the store in call order without waiting on each other, so calls that
finish together share a commit.
301 changes: 255 additions & 46 deletions src/server/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,13 @@ impl Lift {
}
}
const LIFT_GRACE: Duration = Duration::from_millis(250);
/// The most of one round's calls a bot runs at once. Each command also
/// waits for the daemon's process budget, so this is a bot's share of it.
const PARALLEL_CALLS: usize = 4;

/// A tool's result, or the command a steer moved to the background.
enum Ran<F> {
Done(Result<Outcome>),
enum Ran<F, T = Outcome> {
Done(Result<T>),
Lifted(std::pin::Pin<Box<F>>),
}

Expand Down Expand Up @@ -3151,14 +3154,14 @@ impl Turn {
/// the run to the background instead, handed back unfinished, once the
/// paired signal says it is running: a command still waiting for a
/// process slot stays in line in the foreground.
async fn run_tool<F: Future<Output = Result<Outcome>>>(
async fn run_tool<T, F: Future<Output = Result<T>>>(
&self,
run: F,
lift: Option<(&Lift, oneshot::Receiver<()>)>,
warm: Option<&mut Warm<'_>>,
) -> Result<Ran<F>> {
enum Next {
Ran(Result<Outcome>),
) -> Result<Ran<F, T>> {
enum Next<T> {
Ran(Result<T>),
Lifted,
Refreshed(Result<(agent_runtime::provider::Usage, tokio::time::Instant)>),
}
Expand Down Expand Up @@ -3365,6 +3368,26 @@ impl Turn {
let (turn, allowed) = (self.turn, record.callable());
let mut calls = calls.into_iter();
while let Some(call) = calls.next() {
// Calls in a row that only read or run commands, none of them
// gated, run together, as Codex runs them.
if let Some(first) = self.parallel(record, &call, workspace)
&& calls
.as_slice()
.first()
.is_some_and(|next| self.parallel(record, next, workspace).is_some())
{
let mut batch = vec![(call, first)];
while let Some(prepared) = calls
.as_slice()
.first()
.and_then(|next| self.parallel(record, next, workspace))
{
batch.push((calls.next().expect("a next call"), prepared));
}
self.execute_parallel(batch, record, workspace, environment, warm.as_deref_mut())
Comment thread
lydakis marked this conversation as resolved.
.await?;
continue;
}
// An escape from a sandbox runs unconfined only once a gate
// allowed it.
let approved = record.gated(&call, workspace);
Expand Down Expand Up @@ -3419,46 +3442,10 @@ impl Turn {
run it without sandbox_permissions",
))
}
Ok(prepared) if confined => {
// Reading the roots touches the filesystem: off the
// runtime thread.
let (settings, daemon, bot) = (
record.settings.clone(),
self.registry.daemon().clone(),
record.bot_id,
);
let root = record
.workspace
.as_ref()
.map_or_else(|| workspace.to_owned(), PathBuf::from);
tokio::task::spawn_blocking(move || {
// The probe for a sandbox runs once, here off the
// runtime thread, also for a bot from before a restart.
agent_runtime::sandbox::available()?;
// The bot's own socket, so its commands may use the
// daemon as far as a sandboxed bot may; opened first,
// so its folder is there to be kept read-only.
let capability = daemon.capability(bot)?;
let policy = Policy::new(
settings.sandbox,
Some(&root),
&settings.writable,
settings.network,
&daemon,
)?;
Ok(policy.map(|policy| Policy {
capability,
..policy
}))
})
.await
.map_err(|_| Error::new("sandbox_unavailable"))
.flatten()
.map(|policy| {
sandbox = policy.map(Arc::new);
prepared
})
}
Ok(prepared) if confined => self.sandbox(record, workspace).await.map(|policy| {
sandbox = policy;
prepared
}),
other => other,
};
let answered = record
Expand Down Expand Up @@ -3641,6 +3628,228 @@ impl Turn {
Ok(None)
}

/// The call prepared, when it may run beside others: an ungated
/// command in the foreground or a file read, which the bot may call. Codex
/// runs these together too; a change to a file runs alone, in order.
/// A sandboxed bot's command runs beside others only inside its
/// sandbox; one that asks to leave it goes the lone way, to be refused.
fn parallel(
&self,
record: &Bot,
call: &ToolCall,
workspace: &std::path::Path,
) -> Option<Prepared> {
if !matches!(call.name.as_str(), "shell" | "read")
|| record.gated(call, workspace)
|| record.declared(&call.name).is_some()
|| !record.callable().iter().any(|name| name == &call.name)
|| (call.name == "shell" && record.sandboxed() && !record.confines(call, workspace))
{
return None;
}
match self.registry.prepare(&call.name, &call.arguments) {
// A stored stream or result is paged to fit the context left,
// which a read beside it would spend too: it runs alone.
Ok(
prepared @ (Prepared::Shell {
background: false, ..
}
| Prepared::Read {
source: ReadSource::Path(_),
..
}),
) => Some(prepared),
_ => None,
}
}

/// Run a batch of calls [`Self::parallel`] admits together, up to
/// [`PARALLEL_CALLS`] at a time and no more than half the process
/// budget: all started in one commit, each result recorded in call
/// order as soon as it and those before it are done. A steer moves every command still running to the
/// background, as it does a lone one.
async fn execute_parallel(
&self,
batch: Vec<(ToolCall, Prepared)>,
record: &Bot,
workspace: &std::path::Path,
environment: &[(String, String)],
warm: Option<&mut Warm<'_>>,
) -> Result<()> {
let turn = self.turn;
let starting: Vec<ToolCall> = batch.iter().map(|(call, _)| call.clone()).collect();
self.store
.op("tool_start", move |db| {
starting
.iter()
.try_for_each(|call| db.tool_start(turn, call).map(drop))
})
.await?;
// A sandboxed bot's commands all run in its one sandbox; when it
// can't be had, each command fails with why.
let shells = batch
.iter()
.any(|(_, prepared)| matches!(prepared, Prepared::Shell { .. }));
let sandbox = match shells && record.sandboxed() {
true => self.sandbox(record, workspace).await,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

P1 Badge Security: Rebuild the sandbox policy before every shell spawn

With workspace-write enabled on Linux, a sandboxed bot can create inherited helpers rooted at /tmp and /tmp/x and replace /tmp/x with a symlink between calls. This batch constructs Policy once and clones it, so a later shell skips root()’s symlink recheck; the stale path reaches bwrap --bind, making an arbitrary host directory writable without escalation approval. The restricted socket permits children in caller-writable /tmp, and process-slot saturation can delay the later spawn until after the swap. Rebuild and revalidate the policy immediately before every shell spawn.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fixing this in this PR. It needs a hostile bot to swap a root for a symlink in the window between the batch's policy check and a later spawn, and the project's stance is to log sandbox edges like this rather than hold a release for them. It's logged in #96. The fix is small: build the policy per shell call, the way the lone path does.


Generated by Claude Code

false => Ok(None),
};
let runs = batch.into_iter().map(|(call, prepared)| {
let sandbox = match (&prepared, &sandbox) {
(Prepared::Shell { .. }, Err(error)) => Err(error.clone()),
(_, sandbox) => Ok(sandbox.clone().unwrap_or_default()),
};
self.run_parallel(call.call_id, prepared, workspace, environment, sandbox)
});
let record = async {
// Results go to the store in call order as they come, each
// queued without waiting on the one before, so results done
// together share a commit. Waiting for room on the store's
// queue happens beside the calls still running, which go on
// being polled; meanwhile the channel holds at most one result
// per call in the batch.
let (done, mut recorded) = tokio::sync::mpsc::unbounded_channel();
let run = async move {
let mut results = stream::iter(runs).buffered(self.registry.wide(PARALLEL_CALLS));
while let Some((id, outcome)) = results.next().await {
let _ = done.send((id, outcome?));
}
Ok::<_, Error>(())
};
// Each write's acknowledgement carries its result: dropped as
// it comes, so committed results aren't held for the batch.
let write = async {
let mut written = stream::FuturesOrdered::new();
loop {
tokio::select! {
Some(answer) = written.next(), if !written.is_empty() => drop(answer?),
next = recorded.recv() => match next {
Some((id, outcome)) => written.push_back(
self.store.slot().await?.op("tool_finish", move |db| {
db.tool_finish(turn, &id, &outcome)
}),
),
None => break,
},
}
}
while let Some(answer) = written.next().await {
answer?;
}
Ok::<_, Error>(())
};
let (ran, wrote) = tokio::join!(run, write);
ran.and(wrote)
};
match self.run_tool(record, None, warm).await? {
Ran::Done(result) => result,
Ran::Lifted(_) => unreachable!("a batch is not lifted whole"),
}
}

/// One call of a batch, run to its outcome: an error only when the
/// scheduler closed.
async fn run_parallel(
&self,
call_id: String,
prepared: Prepared,
workspace: &std::path::Path,
environment: &[(String, String)],
sandbox: Result<Option<Arc<Policy>>>,
) -> (String, Result<Outcome>) {
let sandbox = match sandbox {
Ok(sandbox) => sandbox,
Err(error) => return (call_id, Ok(failure(error))),
};
let ran = match prepared {
Prepared::Shell {
command,
timeout_ms,
background: false,
} => {
let (started, running) = oneshot::channel();
let mut run = Box::pin(self.registry.foreground(
command,
workspace.to_path_buf(),
timeout_ms,
environment.to_vec(),
sandbox,
started,
));
let lifted = async {
// A run that ends before it starts wins the race below.
match running.await {
Ok(()) => self.lift.lifted().await,
Err(_) => std::future::pending().await,
}
};
let finished = tokio::select! {
biased;
result = &mut run => Some(result),
() = lifted => None,
};
match finished {
Some(result) => result,
None => return (call_id.clone(), self.lift_shell(&call_id, run).await),
}
}
prepared => {
self.registry
.execute(prepared, workspace, environment, sandbox)
.await
}
};
let outcome = match ran {
Ok(outcome) => Ok(annotate(outcome, self.turn, &call_id)),
Err(error) if error.code == "tool_scheduler_closed" => Err(error),
Err(error) => Ok(failure(error)),
};
(call_id, outcome)
}

/// The bot's sandbox, built from its settings. Reading its roots
/// touches the filesystem, so it runs off the runtime thread; it fails
/// rather than leave a call that must be confined unconfined.
async fn sandbox(
&self,
record: &Bot,
workspace: &std::path::Path,
) -> Result<Option<Arc<Policy>>> {
let (settings, daemon, bot) = (
record.settings.clone(),
self.registry.daemon().clone(),
record.bot_id,
);
let root = record
.workspace
.as_ref()
.map_or_else(|| workspace.to_owned(), PathBuf::from);
tokio::task::spawn_blocking(move || {
// The probe for a sandbox runs once, here off the runtime
// thread, also for a bot from before a restart.
agent_runtime::sandbox::available()?;
// The bot's own socket, so its commands may use the daemon as
// far as a sandboxed bot may; opened first, so its folder is
// there to be kept read-only.
let capability = daemon.capability(bot)?;
let policy = Policy::new(
settings.sandbox,
Some(&root),
&settings.writable,
settings.network,
&daemon,
)?;
Ok(policy.map(|policy| Policy {
capability,
..policy
}))
})
.await
.map_err(|_| Error::new("sandbox_unavailable"))
.flatten()
.map(|policy| policy.map(Arc::new))
}

/// Wait for a gated call's verdict: live for the hold, then parked like
/// `wait`, holding no task and no slot. Answers wake the task through
/// the worker, so each check is one job and no commit.
Expand Down
Loading