F3: Kimi inference pipeline + full grok cloud-surface excision

Sampler / inference (PRD F3):
- kimi_compat.rs: single adaptation point for the Kimi chat/completions
  dialect (thinking-field mapping, model_id stripping, empty-content
  tool-call message fix, stream_options.include_usage), with kimi-cli
  source citations
- Rate-limit handling reworked for Kimi/Moonshot semantics; UA kigi/{version}
- /models replaces the xAI models-v2 endpoint everywhere; idle model
  refresh carries X-Msh-* device headers only (X-XAI-Token-Auth and
  x-grok-client-mode/CLIENT_MODE_HEADER machinery deleted)

Cloud-surface excision (PRD §5, zero-egress):
- remote/ conversations lane, cli-chat-proxy-types crate, prod/ dir,
  share command, credit bar: deleted (single local session lane;
  paginate() replaces merge_and_paginate)
- Subscription/tier gate stack deleted end-to-end: AppView
  gate/tier/team/ZDR fields, app/subscription.rs watch loop,
  dispatch/billing.rs paywall + SuperGrok upsell, free-usage-exhausted
  chain, tier-restricted commands, GateInfo, RemoteSettings gate fields,
  SettingsUpdateNotification gate fields
- /privacy + coding-data-sharing setting deleted (backed by a dead xAI
  RPC; Kigi is zero-egress — nothing to share or retain remotely)

Auth UX correctness (user-reported):
- Device-flow fixtures now mirror the live Kimi payload shape
  (https://www.kimi.com/code/authorize_device?user_code=..., verified
  against auth.kimi.com); the fabricated auth.kimi.com/device?code=...
  URLs are gone
- open_browser_detached is a no-op under cfg(test): unit tests drove
  wiremock fixture URLs into the real browser (root cause of the
  "garbage mock link" ABCD-1234 tabs)
- Welcome/pager-minimal rebrand: Grok Build -> Kigi, grok.com ->
  kimi.com, "Sign in to Grok" -> "Sign in to Kimi"
This commit is contained in:
ZacharyZhang-NY committed 2026-07-17 16:05:51 -04:00
1 parent fe1f885bb3
commit ea0ce9d15f
231 files changed
+4730 -26358

No files matched your search

@@ -80,8 +80,6 @@ pub(crate) fn handle_ask_user_question(
let cmd = match kind {
LocalQuestionKind::Fork { .. } => "/fork",
LocalQuestionKind::NewSession => "/new",
LocalQuestionKind::CreditLimitUpsell => "credit-limit upsell",
LocalQuestionKind::FreeUsageUpsell => "SuperGrok upsell",
LocalQuestionKind::AgentTypeMismatch { .. } => "model switch",
LocalQuestionKind::ProjectSelect { .. } => "project select",
};
@@ -67,8 +67,6 @@ use prompt_origin::{push_wake_end_marker, viewer_turn_anchor, wake_turn_elapsed}
pub(crate) use subagent_activity::finalize_killed_subagent;
use subagent_activity::{subagent_activity_label, sync_subagent_activity};
#[cfg(test)]
pub(crate) use session_notification::apply_session_event_for_test;
use session_notification::{
advance_reconnect_cursor, confirm_context_used, detect_plan_mode_change,
drop_unexpected_replay, handle_session_notification,
@@ -342,8 +342,6 @@ pub(super) fn handle_session_notification(notif: &acp::ExtNotification, app: &mu
restore_degree: None,
rate_limited: false,
model_incompatible: false,
credit_limit_blocked: false,
free_usage_blocked: false,
bg_tasks: std::collections::BTreeMap::new(),
bg_tool_call_to_task: std::collections::HashMap::new(),
scheduled_tasks: std::collections::HashMap::new(),
@@ -363,7 +361,6 @@ pub(super) fn handle_session_notification(notif: &acp::ExtNotification, app: &mu
child_view.set_input_mode(InputMode::Vim);
child_view.is_subagent_view = true;
child_view.active_pane = crate::views::agent::ActivePane::Scrollback;
child_view.set_sharing_enabled(agent.sharing_enabled);
let usage_visible = agent
.prompt
.slash_controller
@@ -1087,18 +1084,6 @@ pub(super) fn handle_child_session_notification(
/// Apply a compaction or retry event to a session's activity state and scrollback.
///
/// Shared between the root agent and child (subagent) notification paths.
/// Test-only shim so dispatch-level tests can replay real notification
/// sequences (e.g. `RetryState::Retrying` → `Exhausted`) through the
/// production handler — the Retrying arm clears the `in_flight_prompt`
/// rewind stash, which a fixture setting fields directly would miss.
#[cfg(test)]
pub(crate) fn apply_session_event_for_test(
update: &XaiSessionUpdate,
session: &mut AgentSession,
scrollback: &mut crate::scrollback::state::ScrollbackState,
) -> bool {
apply_session_event(update, session, scrollback, false)
}
pub(super) fn apply_session_event(
update: &XaiSessionUpdate,
session: &mut AgentSession,
@@ -1215,7 +1200,6 @@ pub(super) fn apply_retry_state(
scrollback: &mut crate::scrollback::state::ScrollbackState,
is_api_key_auth: bool,
) {
let mut is_credit_limit = false;
let mut is_reauth = false;
use kigi_shell::extensions::notification::RetryState;
match retry {
@@ -1238,14 +1222,7 @@ pub(super) fn apply_retry_state(
session.set_retry_activity(None);
session.rate_limited = *rate_limited;
is_credit_limit = super::super::dispatch::is_credit_limit_error(None, reason);
let is_free_usage =
*rate_limited && super::super::dispatch::is_free_usage_exhausted_error(reason);
if is_credit_limit {
session.credit_limit_blocked = true;
} else if is_free_usage {
session.free_usage_blocked = true;
} else if !*rate_limited && is_reauthable_failure(None, reason) {
if !*rate_limited && is_reauthable_failure(None, reason) {
is_reauth = true;
scrollback.push_block(RenderBlock::session_event(SessionEvent::ReAuthRequired));
} else {
@@ -1268,10 +1245,7 @@ pub(super) fn apply_retry_state(
if error_type == "encrypted_content_mismatch" {
session.model_incompatible = true;
}
is_credit_limit = super::super::dispatch::is_credit_limit_error(None, message);
if is_credit_limit {
session.credit_limit_blocked = true;
} else if is_reauthable_failure(Some(error_type.as_str()), message) {
if is_reauthable_failure(Some(error_type.as_str()), message) {
is_reauth = true;
scrollback.push_block(RenderBlock::session_event(SessionEvent::ReAuthRequired));
} else if error_type == "context_length" {
@@ -1287,8 +1261,7 @@ pub(super) fn apply_retry_state(
}
}
}
if is_credit_limit {
} else if !is_reauth {
if !is_reauth {
session.in_flight_prompt = None;
}
}
@@ -107,23 +107,6 @@ pub(super) fn handle_settings_update(notif: &acp::ExtNotification, app: &mut App
if let Some(v) = update.show_resolved_model {
app.show_resolved_model = v;
}
if let Some(v) = update.sharing_enabled {
app.sharing_enabled = v;
// Propagate to existing agents so slash-command registries stay
// in sync (same fan-out pattern used when creating new agents).
for agent in app.agents.values_mut() {
agent.set_sharing_enabled(v);
}
}
// Always recompute is_api_key_auth from the tier so a later Free/SuperGrok
// stamp does not leave API-key bypass / hidden `/usage` stuck.
if let Some(v) = update.subscription_tier_display {
let is_key = super::super::app_view::is_api_key_label(&v);
app.is_api_key_auth = is_key;
app.usage_visible = !is_key && app.team_name.is_none();
app.subscription_tier = Some(v);
app.apply_tier_restrictions();
}
// TODO: extract resolve_session_picker_grouped helper (duplicates event_loop.rs:143-160)
// Respect env var > config > remote precedence (mirrors event_loop.rs startup).
if let Some(remote_val) = update.session_picker_grouped {
@@ -142,33 +125,6 @@ pub(super) fn handle_settings_update(notif: &acp::ExtNotification, app: &mut App
.unwrap_or(remote_val);
app.session_picker_grouped = resolved;
}
if let Some(v) = update.subscription_watch_interval_secs {
app.subscription_watch_interval_secs = Some(v);
}
// Gate update logic:
// - allow_access == Some(true): explicitly granted → lift the gate
// - gate_message.is_some(): server sent a new message → impose/update
// - Neither condition met: don't touch the gate. In particular,
// allow_access=Some(false) without a gate_message must NOT clear the
// gate (gate_from_settings returns None when gate_message is absent,
// which would incorrectly lift an existing gate).
if update.allow_access == Some(true) {
let effs = app.lift_gate();
app.pending_effects.extend(effs);
} else if let Some(msg) = update.gate_message.as_ref()
&& !msg.is_empty()
{
// (An empty gate_message would only clear the gate message text, NOT
// access, so it intentionally does not touch the gate here.)
let effs = app.impose_gate(kigi_shell::auth::GateInfo {
message: msg.clone(),
url: update.gate_url.clone(),
label: update.gate_label.clone(),
});
app.pending_effects.extend(effs);
}
// Load config layers once for tips + group_tool_verbs +
// collapsed_edit_blocks resolution. Loaded unconditionally: the UI flags
// re-resolve on every update (see below), and updates are rare (post-auth
@@ -361,22 +317,10 @@ pub(super) struct PagerSettingsUpdate {
#[serde(default)]
show_resolved_model: Option<bool>,
#[serde(default)]
sharing_enabled: Option<bool>,
#[serde(default)]
session_picker_grouped: Option<bool>,
#[serde(default)]
tips: Option<Vec<String>>,
#[serde(default)]
gate_message: Option<String>,
#[serde(default)]
gate_url: Option<String>,
#[serde(default)]
gate_label: Option<String>,
#[serde(default)]
allow_access: Option<bool>,
#[serde(default)]
subscription_tier_display: Option<String>,
#[serde(default)]
auto_permission_mode_enabled: Option<bool>,
/// Soft-default permission mode. Presence-aware: omit = no update,
/// `null` = recompute with remote=None, string = that soft-default.
@@ -389,8 +333,6 @@ pub(super) struct PagerSettingsUpdate {
group_tool_verbs: Option<bool>,
#[serde(default)]
collapsed_edit_blocks: Option<bool>,
#[serde(default)]
subscription_watch_interval_secs: Option<u64>,
}
/// Presence-aware string: omit → `None` (`#[serde(default)]`), null →
@@ -33,8 +33,6 @@ pub(super) fn make_session(session_id: Option<&str>) -> AgentSession {
restore_degree: None,
rate_limited: false,
model_incompatible: false,
credit_limit_blocked: false,
free_usage_blocked: false,
available_commands: Vec::new(),
available_commands_generation: 0,
available_tools: None,
@@ -257,17 +255,6 @@ pub(super) fn follow_ups_ext_with_prompt(
std::sync::Arc::from(serde_json::value::to_raw_value(&params).unwrap()),
)
}
pub(super) fn tier_settings_update(tier: &str) -> acp::ExtNotification {
acp::ExtNotification::new(
"x.ai/settings/update",
std::sync::Arc::from(
serde_json::value::to_raw_value(
&serde_json::json!({ "subscription_tier_display" : tier }),
)
.unwrap(),
),
)
}
pub(super) fn group_tool_verbs_settings_update(
value: Option<bool>,
) -> acp::ExtNotification {
@@ -119,7 +119,7 @@
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
seed_pending_tool(agent, "create-plan-call", "CreatePlan");
agent.active_modal = Some(crate::views::modal::ActiveModal::CommandPalette {
entries: crate::views::modal::default_palette_entries(agent.sharing_enabled),
entries: crate::views::modal::default_palette_entries(),
state: crate::views::picker::PickerState::input_active(),
window: crate::views::modal_window::ModalWindowState::new(),
});
@@ -1751,24 +1751,7 @@
}
/// The credit-limit early return discards the popped adoption's buffer.
#[test]
fn credit_limit_response_discards_adoption_buffer() {
let mut app = app_with_running_p1_and_stashed_b1();
let id = AgentId(0);
send_tool_call_update(&mut app, "b1", "bash-mode-1", None);
app.agents
.get_mut(&id)
.unwrap()
.session
.credit_limit_blocked = true;
prompt_response(&mut app, "p1");
let agent = app.agents.get(&id).unwrap();
assert!(!app.pending_running_adoptions.contains_key(&id));
assert!(agent.pending_adoption_updates.is_empty());
}
/// A stash whose pid replayed a durable terminal is discarded, never adopted.
/// A stash whose pid replayed a durable terminal is discarded, never adopted.
#[test]
fn terminal_in_replay_stash_is_discarded_not_adopted() {
let mut app = app_with_running_p1_and_stashed_b1();
@@ -157,7 +157,7 @@
apply_retry_state(&exhausted, &mut session, &mut scrollback, false);
match last_session_event(&scrollback) {
Some(SessionEvent::RetryFailed { error, .. }) => {
assert_eq!(error, RATE_LIMITED_USER_MESSAGE_OAUTH);
assert_eq!(error, RATE_LIMITED_USER_MESSAGE_OAUTH.as_str());
}
other => panic!("expected OAuth rate-limit RetryFailed, got {other:?}"),
}
@@ -194,140 +194,8 @@
);
}
/// A rate-limit exhaustion whose flattened reason carries the
/// free-usage code sets both flags and pushes NO generic block (the
/// driver shows the paywall modal on PromptResponse; viewers keep no
/// marker).
#[test]
fn retry_exhausted_free_usage_sets_paywall_flag_without_marker() {
let mut session = make_session(Some("s1"));
let mut scrollback = ScrollbackState::new();
session.in_flight_prompt = Some(InFlightPrompt {
text: "try me again".into(),
images: Vec::new(),
scrollback_entry: EntryId::new(2),
chip_elements: Vec::new(),
});
apply_retry_state(
&RetryState::Exhausted {
attempts: 0,
reason: "API error (status 429 Too Many Requests): \
subscription:free-usage-exhausted: You have used all your free usage."
.into(),
is_rate_limited: true,
},
&mut session,
&mut scrollback,
false,
);
assert!(
session.rate_limited,
"free-usage keeps rate_limited (TurnFailed/toast suppression)"
);
assert!(session.free_usage_blocked);
assert_eq!(
scrollback.len(),
0,
"no RetryFailed marker — the paywall modal shows instead"
);
assert!(
session.in_flight_prompt.is_none(),
"free-usage exhaustion clears in_flight_prompt like other failures"
);
}
#[test]
fn apply_retry_state_credit_limit_exhausted_preserves_in_flight_prompt() {
let mut session = make_session(Some("s1"));
let mut scrollback = ScrollbackState::new();
session.in_flight_prompt = Some(InFlightPrompt {
text: "stash me".into(),
images: Vec::new(),
scrollback_entry: EntryId::new(2),
chip_elements: Vec::new(),
});
apply_retry_state(
&RetryState::Exhausted {
attempts: 3,
reason: "status 403: run out of credits".into(),
is_rate_limited: false,
},
&mut session,
&mut scrollback,
false,
);
assert!(
session.credit_limit_blocked,
"credit_limit_blocked must be set for credit-limit 403"
);
assert!(
session.in_flight_prompt.is_some(),
"in_flight_prompt must be preserved so PromptResponse handler can stash it"
);
assert_eq!(session.in_flight_prompt.unwrap().text, "stash me");
}
#[test]
fn apply_retry_state_credit_limit_failed_preserves_in_flight_prompt() {
let mut session = make_session(Some("s1"));
let mut scrollback = ScrollbackState::new();
session.in_flight_prompt = Some(InFlightPrompt {
text: "stash me too".into(),
images: Vec::new(),
scrollback_entry: EntryId::new(3),
chip_elements: Vec::new(),
});
apply_retry_state(
&RetryState::Failed {
error_type: "proxy_error".into(),
message: "status 403: run out of credits".into(),
},
&mut session,
&mut scrollback,
false,
);
assert!(
session.credit_limit_blocked,
"credit_limit_blocked must be set for credit-limit 403"
);
assert!(
session.in_flight_prompt.is_some(),
"in_flight_prompt must be preserved so PromptResponse handler can stash it"
);
assert_eq!(session.in_flight_prompt.unwrap().text, "stash me too");
}
#[test]
fn apply_retry_state_pool_402_sets_credit_limit_blocked() {
let mut session = make_session(Some("s1"));
let mut scrollback = ScrollbackState::new();
session.in_flight_prompt = Some(InFlightPrompt {
text: "pool blocked".into(),
images: Vec::new(),
scrollback_entry: EntryId::new(5),
chip_elements: Vec::new(),
});
apply_retry_state(
&RetryState::Failed {
error_type: "proxy_error".into(),
message:
"API error (status 402 Payment Required): Grok Build usage balance exhausted"
.into(),
},
&mut session,
&mut scrollback,
false,
);
assert!(
session.credit_limit_blocked,
"credit_limit_blocked must be set for pool 402 balance exhausted"
);
assert!(session.in_flight_prompt.is_some());
}
#[test]
fn apply_retry_state_non_credit_limit_failed_clears_in_flight_prompt() {
fn apply_retry_state_generic_failed_clears_in_flight_prompt() {
let mut session = make_session(Some("s1"));
let mut scrollback = ScrollbackState::new();
session.in_flight_prompt = Some(InFlightPrompt {
@@ -345,13 +213,9 @@
&mut scrollback,
false,
);
assert!(
!session.credit_limit_blocked,
"credit_limit_blocked must NOT be set for non-credit-limit errors"
);
assert!(
session.in_flight_prompt.is_none(),
"in_flight_prompt must be cleared for non-credit-limit errors"
"in_flight_prompt must be cleared for generic errors"
);
}
@@ -400,7 +264,6 @@
),
"auth 401 must surface the actionable re-auth prompt"
);
assert!(!session.credit_limit_blocked);
}
/// A recoverable auth failure preserves `in_flight_prompt` so the
@@ -1,41 +1,6 @@
#![cfg_attr(rustfmt, rustfmt::skip)]
use super::*;
#[test]
fn settings_non_api_key_tier_clears_stale_api_key_flag() {
let mut app = make_app_with_agent("sess-stale-key");
assert!(handle_ext_notification(
&tier_settings_update("API Key"),
&mut app
));
assert!(app.is_api_key_auth);
assert!(!app.usage_visible);
assert!(app.tier_restricted_commands.is_empty());
// Later personal Free stamp must not keep the API-key bypass.
assert!(handle_ext_notification(
&tier_settings_update("Free"),
&mut app
));
assert!(!app.is_api_key_auth);
assert!(app.usage_visible);
// Tier gating no longer exists; nothing gets re-restricted.
assert!(app.tier_restricted_commands.is_empty());
// A paid tier after API Key clears the api-key flag and tier limits.
let mut app = make_app_with_agent("sess-paid-tier");
assert!(handle_ext_notification(
&tier_settings_update("API Key"),
&mut app
));
assert!(handle_ext_notification(
&tier_settings_update("SuperGrok"),
&mut app
));
assert!(!app.is_api_key_auth);
assert!(app.tier_restricted_commands.is_empty());
}
#[test]
fn settings_update_clearing_group_tool_verbs_reverts_to_default() {
// Expected values come from the same chain the handler resolves, so the