Synced from monorepo

Changes:
- Stop hooks for session lifecycle
- Add x.ai/session/state and x.ai/session/import ACP methods
- Deny-and-continue for auto-mode classifier blocks with denial limits
- Drop codebase-upload from dhat soak test
- scheduler_create upsert via task_id; retire one-shot tasks
- Clipboard: copy file fallback + honest toasts for SSH/Apple Terminal
- Polarity-safe syntax colors in minimal mode
- Auto mode classifies unvetted env prefixes instead of hard-prompting
- Add GROK_CLIPBOARD_NO_OSC52 kill switch to force OSC 52 off
This commit is contained in:
grokkybara[bot] 2026-07-19 18:40:33 +01:00
commit ba76b0a683
143 changed files with 9465 additions and 3419 deletions

View file

@ -368,6 +368,9 @@ impl ChatStateActor {
ChatStateCommand::GetLastAssistantText { reply } => {
let _ = reply.send(self.get_last_assistant_text());
}
ChatStateCommand::GetLastAssistantTextInTurn { reply } => {
let _ = reply.send(self.get_last_assistant_text_in_turn());
}
ChatStateCommand::GetFirstUserText { reply } => {
let _ = reply.send(self.get_first_user_text());
}

View file

@ -151,6 +151,37 @@ impl ChatStateActor {
})
}
/// Return the current turn's last assistant message with non-empty text, or
/// `None` when the turn produced none.
///
/// Like [`Self::get_last_assistant_text`], but the backwards walk stops at the
/// turn boundary (a user item with `prompt_index` set, a genuine user message,
/// or a synthetic reason with [`SyntheticReason::starts_prompt_turn`]); mid-turn
/// synthetic injections are walked past.
///
/// [`SyntheticReason::starts_prompt_turn`]: xai_grok_sampling_types::SyntheticReason::starts_prompt_turn
pub(super) fn get_last_assistant_text_in_turn(&self) -> Option<String> {
for item in self.state.conversation.iter().rev() {
match item {
xai_grok_sampling_types::ConversationItem::Assistant(a)
if !a.content.trim().is_empty() =>
{
return Some(a.content.as_ref().to_owned());
}
xai_grok_sampling_types::ConversationItem::User(u)
if u.prompt_index.is_some()
|| u.synthetic_reason
.as_ref()
.is_none_or(|r| r.starts_prompt_turn()) =>
{
return None;
}
_ => {}
}
}
None
}
/// Return the text of the **first content part** of the first `User` message,
/// if and only if that part is `ContentPart::Text`.
///

View file

@ -2716,6 +2716,43 @@ async fn get_last_assistant_text_skips_whitespace_only() {
assert_eq!(text.as_deref(), Some("real answer"));
}
#[tokio::test]
async fn get_last_assistant_text_in_turn_stops_at_boundary() {
let h = TestHarness::new();
h.handle.push_user_message(ConversationItem::user("q1"));
h.handle
.push_assistant_response(ConversationItem::assistant("previous turn answer"));
h.handle.push_user_message(ConversationItem::user("q2"));
assert!(h.handle.get_last_assistant_text_in_turn().await.is_none());
assert_eq!(
h.handle.get_last_assistant_text().await.as_deref(),
Some("previous turn answer"),
"the unbounded sibling still sees prior turns"
);
}
#[tokio::test]
async fn get_last_assistant_text_in_turn_walks_past_synthetic_injections() {
let h = TestHarness::new();
h.handle.push_user_message(ConversationItem::user("q"));
h.handle
.push_assistant_response(ConversationItem::assistant("turn answer"));
h.handle
.push_user_message(ConversationItem::stop_hook_feedback("keep working"));
assert_eq!(
h.handle.get_last_assistant_text_in_turn().await.as_deref(),
Some("turn answer"),
"synthetic mid-turn items must not act as turn boundaries"
);
// A turn-starting synthetic item (auto-wake) IS a boundary.
h.handle
.push_user_message(ConversationItem::task_completed("task done"));
assert!(h.handle.get_last_assistant_text_in_turn().await.is_none());
}
#[tokio::test]
async fn get_last_assistant_text_no_assistant_messages() {
let h = TestHarness::new();

View file

@ -297,6 +297,13 @@ pub enum ChatStateCommand {
reply: oneshot::Sender<Option<String>>,
},
/// Like `GetLastAssistantText`, but bounded to the current prompt turn:
/// returns `None` when the turn produced no assistant text (the walk stops
/// at the first turn-starting user item).
GetLastAssistantTextInTurn {
reply: oneshot::Sender<Option<String>>,
},
/// Get the text of the first `Text` content part in the first `User` message.
/// Returns `None` if the conversation has no user messages or the first user
/// message has no text content part.
@ -461,6 +468,9 @@ mod tests {
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetLastAssistantText { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetLastAssistantTextInTurn { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetFirstUserText { reply: tx };

View file

@ -556,6 +556,20 @@ impl ChatStateHandle {
.flatten()
}
/// Get the current turn's last assistant message text, or `None` when the
/// turn produced none (or the actor is dead). Turn-scoped, unlike
/// [`get_last_assistant_text`], and cheaper than [`get_conversation`].
///
/// [`get_conversation`]: Self::get_conversation
/// [`get_last_assistant_text`]: Self::get_last_assistant_text
pub async fn get_last_assistant_text_in_turn(&self) -> Option<String> {
self.query("GetLastAssistantTextInTurn", |reply| {
ChatStateCommand::GetLastAssistantTextInTurn { reply }
})
.await
.flatten()
}
/// Get the text of the first `Text` content part in the first `User` message.
///
/// Returns `None` if no user message with text content exists or the actor