Synced from monorepo

Synced from monorepo

Changes:
- Temporarily disable session share link creation in the TUI
- Do not approve plan on empty Enter from the revise prompt
- Expose chat product Skills via ACP available_commands_update
- Return immediately from a blocking wait on an already-completed ACP task
- Split headless pager module for clearer structure
- Stop git worktree prune from removing user registrations on resume
- Use compaction sampler tokenizer for item token counts
- Opt-in extra root CAs via GROK_EXTRA_CA_BUNDLE
- Cancel all session subagents when the user stops
- Let the session persistence actor exit when its session ends
- Make fullscreen terminal resize much cheaper on long sessions
- Report honestly from kill_task when an ACP task does not exist
- Hide /usage for external-auth deployments
- Forward the history-load trailer’s computer_reason to the client
- Remove ineffective no-op tool reminder
- Declare slash-command screen-mode support in one place
- Keep settings enum picker on the committed value until Enter
- Reap a PTY’s full process tree
- Stream tool calls from headless mode over ACP
- Bridge gateway task lifecycle to ACP for chat session background tasks
- Don’t warn about truncated history on a suppressed replay
- Fit full-replace summarizer input and recover on context-length errors
- Stop dropping agents over an unrecognized frontmatter color
- Add /undo as a slash alias for /rewind
- Harden sleep/wake token-refresh paths against forced re-login
- Add session/list ACP method
- Give each sampling backend its own conversion module
- Treat an unenrolled child process as a lint error
- Suppress the cancelled marker on send-now wake turns
- Stop tearing down Roslyn on every edit, and read C# diagnostics

Source-Revision: 2a28b4a86cfc4a4c133c35b7fc2a6a9964387c39
This commit is contained in:
grokkybara[bot] 2026-07-30 19:07:40 +00:00
commit dd04f397b1
367 changed files with 29489 additions and 10051 deletions

View file

@ -91,7 +91,7 @@ impl std::fmt::Debug for EventWriter {
mod tests {
use super::*;
use crate::events::types::{
EVENT_SCHEMA_VERSION, Event, SessionRelationship, TurnOutcomeLabel,
EVENT_SCHEMA_VERSION, Event, SessionRelationship, ToolOutcome, TurnOutcomeLabel,
};
fn _assert_event_writer_is_send_sync_clone()
@ -116,6 +116,13 @@ mod tests {
redirect_kind: None,
});
writer.emit(Event::FirstToken);
writer.emit(Event::ToolCompleted {
tool_name: "bash".into(),
duration_ms: 1500,
outcome: ToolOutcome::Success,
tool_call_id: "call_xyz".into(),
source: crate::events::types::ToolCompletedSource::Shell,
});
writer.emit(Event::TurnEnded {
outcome: TurnOutcomeLabel::Completed,
cancellation_category: None,
@ -124,7 +131,7 @@ mod tests {
let text = std::fs::read_to_string(dir.path().join("events.jsonl")).unwrap();
let lines: Vec<&str> = text.trim().split('\n').collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines.len(), 4);
let first: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
assert_eq!(first["type"], "turn_started");
@ -135,9 +142,19 @@ mod tests {
assert_eq!(second["type"], "first_token");
let third: serde_json::Value = serde_json::from_str(lines[2]).unwrap();
assert_eq!(third["type"], "turn_ended");
assert_eq!(third["outcome"], "completed");
assert!(third.get("cancellation_category").is_none());
assert_eq!(third["type"], "tool_completed");
assert_eq!(third["tool_name"], "bash");
assert_eq!(third["duration_ms"], 1500);
assert_eq!(third["tool_call_id"], "call_xyz");
assert!(
third.get("source").is_none(),
"shell ToolCompleted must omit source"
);
let fourth: serde_json::Value = serde_json::from_str(lines[3]).unwrap();
assert_eq!(fourth["type"], "turn_ended");
assert_eq!(fourth["outcome"], "completed");
assert!(fourth.get("cancellation_category").is_none());
}
#[test]

View file

@ -8,5 +8,6 @@ pub use log::EventWriter;
pub use tracker::EventTracker;
pub use types::{
CancellationCategory, EVENT_SCHEMA_VERSION, Event, McpConfigServer, McpErrorCategory,
PermissionDecision, Phase, SessionRelationship, ToolOutcome, TurnOutcomeLabel,
PermissionDecision, Phase, SessionRelationship, ToolCompletedSource, ToolOutcome,
TurnOutcomeLabel,
};

View file

@ -5,12 +5,21 @@ use std::time::Instant;
use super::log::EventWriter;
use super::types::{CancellationCategory, Event, RedirectKind, TurnOutcomeLabel};
/// In-flight tool for cancel telemetry. Duration is the dispatch wall already
/// measured, so cancel can reuse it instead of re-timing post-flight.
#[derive(Debug, Clone)]
struct ActiveTool {
tool_name: String,
tool_call_id: String,
dispatch_duration_ms: u64,
}
/// Per-session event state. `!Send` — lives on the session actor.
/// Background tasks use `tracker.writer()` to get a `Clone + Send + Sync` handle.
pub struct EventTracker {
writer: EventWriter,
turn_ended_emitted: Cell<bool>,
active_tool: RefCell<Option<(String, Instant)>>,
active_tool: RefCell<Option<ActiveTool>>,
turn_tool_count: Cell<u32>,
/// Cross-turn one-shot: the *fatal* user-interrupt cause that cancelled the
/// most recent turn (set by the cancel paths), consumed by the *next* real
@ -42,7 +51,7 @@ impl std::fmt::Debug for EventTracker {
.field("writer", &self.writer)
.field("turn_ended_emitted", &self.turn_ended_emitted.get())
.field("turn_tool_count", &self.turn_tool_count.get())
.field("active_tool", &active_tool.as_ref().map(|(name, _)| name))
.field("active_tool", &*active_tool)
.field(
"prior_interrupt_category",
&self.prior_interrupt_category.get(),
@ -101,12 +110,21 @@ impl EventTracker {
});
}
/// Set the active tool for cancellation tracking and return the start instant.
pub fn tool_started(&self, tool_name: String) -> Instant {
let now = Instant::now();
*self.active_tool.borrow_mut() = Some((tool_name, now));
self.turn_tool_count.set(self.turn_tool_count.get() + 1);
now
/// Mark a tool as active for cancellation tracking.
///
/// `dispatch_duration_ms` is the wall time already measured for this call, so
/// a cancel can report it rather than re-measure from post-flight.
pub fn tool_started(&self, tool_name: String, tool_call_id: String, dispatch_duration_ms: u64) {
let is_new = self.active_tool.borrow().is_none();
*self.active_tool.borrow_mut() = Some(ActiveTool {
tool_name,
tool_call_id,
dispatch_duration_ms,
});
// Re-entry (e.g. after reauth adds retry wall time) only refreshes duration.
if is_new {
self.turn_tool_count.set(self.turn_tool_count.get() + 1);
}
}
pub fn tool_count_this_turn(&self) -> u32 {
@ -123,12 +141,17 @@ impl EventTracker {
/// Cancel in-flight tool and emit `ToolCompleted(cancelled)`.
/// Called from `cancel_running_task()` before `turn_ended`.
///
/// A tool cancelled while still dispatching was never marked active, so it
/// gets no `tool_completed` row at all.
pub fn cancel_active_tool(&self) {
if let Some((tool_name, start)) = self.active_tool.borrow_mut().take() {
if let Some(tool) = self.active_tool.borrow_mut().take() {
self.emit(Event::ToolCompleted {
tool_name,
duration_ms: start.elapsed().as_millis() as u64,
tool_name: tool.tool_name,
duration_ms: tool.dispatch_duration_ms,
outcome: super::types::ToolOutcome::Cancelled,
tool_call_id: tool.tool_call_id,
source: super::types::ToolCompletedSource::Shell,
});
}
}

View file

@ -40,8 +40,18 @@ pub enum Event {
},
ToolCompleted {
tool_name: String,
/// Dispatch wall time; a cancel row reuses the duration measured at dispatch.
duration_ms: u64,
outcome: ToolOutcome,
/// Model/ACP tool call id; matches the conversation's `tool_result`.
/// Omitted on write when empty.
#[serde(skip_serializing_if = "String::is_empty")]
tool_call_id: String,
/// Which emitter wrote this row. Shell (default) is omitted on the wire
/// and is what package joins should use; workspace rows time the
/// hub/proxy hop for the same call.
#[serde(skip_serializing_if = "ToolCompletedSource::is_shell")]
source: ToolCompletedSource,
},
PermissionRequested {
tool_name: String,
@ -457,6 +467,26 @@ pub enum Event {
},
}
/// Who emitted a [`Event::ToolCompleted`] row.
///
/// Wire: shell is omitted (legacy empty/`source` absent); workspace is
/// `"workspace"`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCompletedSource {
/// Shell dispatch clock — join against these.
#[default]
Shell,
/// Workspace hub/proxy hop clock.
Workspace,
}
impl ToolCompletedSource {
pub fn is_shell(&self) -> bool {
matches!(self, Self::Shell)
}
}
/// Where a mid-turn interjection originated. Drives the `source` field on
/// [`Event::Interjected`].
#[derive(Debug, Clone, Copy, Serialize)]
@ -654,6 +684,29 @@ mod tests {
}
}
#[test]
fn tool_completed_source_omits_shell_writes_workspace() {
let shell = serde_json::to_value(Event::ToolCompleted {
tool_name: "bash".into(),
duration_ms: 10,
outcome: ToolOutcome::Success,
tool_call_id: "c1".into(),
source: ToolCompletedSource::Shell,
})
.unwrap();
assert!(shell.get("source").is_none());
let workspace = serde_json::to_value(Event::ToolCompleted {
tool_name: "bash".into(),
duration_ms: 10,
outcome: ToolOutcome::Success,
tool_call_id: "c1".into(),
source: ToolCompletedSource::Workspace,
})
.unwrap();
assert_eq!(workspace["source"], "workspace");
}
#[test]
fn interjected_event_serializes_tag_source_and_count() {
let ev = Event::Interjected {