fix(TASK-035): close agent-bracket leaks and prove compression/role-hook seams

- Open the top-level agent bracket only at committed dispatch sites so
  inspection-flag early returns (--list-sessions, --info, --macro,
  --rebuild-rag, --init-*, --build-tools) never leak agent.started
- Close the agent bracket ahead of process::exit in shell_execute's
  execute branch, where the dispatch wrapper's close is unreachable
- Extract the ask-loop auto-compression seam and cover the
  session.compressed success path at both call sites with sink tests
- Prove Session.role_hooks never serializes and that injected
  role_hooks YAML is ignored on deserialization
This commit is contained in:
Dark-Alex-17 committed 2026-09-16 13:44:35 -06:00
1 parent 8710737964
commit 0c6b7c8594
3 files changed
+174 -6

No files matched your search

+21 -1
View File
@@ -388,7 +388,12 @@ async fn run(
let app = Arc::clone(&ctx.app.config);
ctx.use_agent(app.as_ref(), agent, session, abort_signal.clone())
.await?;
ctx.top_level_agent_started();
// The top-level agent bracket does NOT open here: the inspection
// flags below (--list-sessions, --info, --macro, --rebuild-rag,
// --init-*) return early without running the agent, and an open
// bracket on those paths would leak `agent.started` with no
// terminal event. It opens at the dispatch sites further down,
// where an agent run is actually committed.
} else {
let app: Arc<AppConfig> = Arc::clone(&ctx.app.config);
if let Some(prompt) = &cli.temp_role {
@@ -525,6 +530,9 @@ async fn run(
return Ok(());
}
if cli.execute && !is_repl {
// Dispatch committed: open the top-level agent bracket only now,
// past every inspection-flag early return above.
ctx.top_level_agent_started();
let result = async {
let input = create_input(&ctx, text, &cli.file, abort_signal.clone()).await?;
shell_execute(&mut ctx, &SHELL, input, abort_signal.clone()).await
@@ -550,6 +558,12 @@ async fn run(
return acp::run_acp_server(ctx, abort_signal).await;
}
// Dispatch committed (headless directive or REPL): open the top-level
// agent bracket only now, past every inspection-flag early return
// above. The ACP server stays unbracketed deliberately. The headless
// arm closes the bracket below; the REPL closes it at teardown (or on
// an interactive agent switch).
ctx.top_level_agent_started();
match is_repl {
false => {
let result = async {
@@ -692,6 +706,12 @@ async fn shell_execute(
if code == 0 && app.save_shell_history {
let _ = append_to_shell_history(&shell.name, &eval_str, code);
}
// `process::exit` never returns, so the dispatch
// wrapper's bracket close in `run()` is unreachable
// from here: close the top-level agent bracket now.
// The agent run itself succeeded — the exit code
// belongs to the user's shell command, not to us.
ctx.top_level_agent_finished(None);
hooks::drain_pending(EXIT_HOOK_DRAIN_TIMEOUT).await;
process::exit(code);
}