docs(comments): rewrite comments across all crates to the guidelines

Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
This commit is contained in:
ZacharyZhang-NY committed 2026-07-23 16:55:39 -04:00
1 parent ff0fb56c67
commit a02b555e66
1458 files changed
+10729 -21750

No files matched your search

@@ -155,7 +155,8 @@ impl AgentActivity {
}
if signaled.iter().all(|(_, tx)| tx.is_closed()) {
return; // nothing to flush, or all actors exited
// nothing to flush, or all actors exited
return;
}
if tokio::time::Instant::now() >= deadline {
for (id, tx) in &signaled {
+70 -87
View File
@@ -41,10 +41,6 @@ use indexmap::IndexMap;
pub struct LeaderAutoUpdateConfig {
/// Interval between update checks (default: 1 hour).
pub check_interval: Duration,
/// Async function that checks for, downloads, and installs an update.
/// Returns `true` if the update was installed successfully and the leader
/// should shut down. Returns `false` to stay alive (no update, or download
/// failed).
pub check_fn:
Box<dyn Fn() -> Pin<Box<dyn std::future::Future<Output = bool> + Send>> + Send + Sync>,
}
@@ -55,8 +51,6 @@ pub struct LeaderAutoUpdateConfig {
/// download request timeout (20 minutes) so the leader does not abandon a
/// transfer that is still within the HTTP client's budget. If the call takes
/// longer than this, we abandon the attempt and retry on the next interval.
/// The select! with the cancellation token ensures the loop remains
/// responsive to shutdown signals even while waiting.
const AUTO_UPDATE_CHECK_TIMEOUT: Duration = Duration::from_secs(20 * 60);
/// How long the auto-update shutdown waits for session actors to flush
@@ -98,8 +92,8 @@ const MAX_AUTO_UPDATE_BUSY_DEFERRALS: u32 = 24;
/// and a timeout so that a stalled download cannot block the loop from
/// responding to shutdown signals.
///
/// This is extracted as a standalone function so it can be unit-tested
/// independently from the full leader infrastructure.
/// A standalone function so it can be unit-tested independently from the full
/// leader infrastructure.
pub(crate) async fn run_auto_update_checker(
config: LeaderAutoUpdateConfig,
agent_busy: Arc<AtomicBool>,
@@ -120,9 +114,6 @@ pub(crate) async fn run_auto_update_checker(
info!("Leader auto-update: running update check");
// Run check_fn inside a select! with cancellation and a timeout so a
// stalled network call cannot block the loop from responding to shutdown.
// The check_fn may include a binary download, so the timeout is generous.
let update_installed = tokio::select! {
biased;
_ = cancel.cancelled() => break,
@@ -226,15 +217,14 @@ fn spawn_agent_local(
/// `kigi/...` extension method, for injection into the agent's inbound ACP
/// stream by the leader's own watcher tasks (config hot-reload, skills).
///
/// The wire method is written **`_`-prefixed** (`_kigi/internal/...`):
/// The wire method must be written `_`-prefixed (`_kigi/internal/...`):
/// `agent-client-protocol`'s inbound decoder routes a non-built-in method to
/// `ext_method` only when it carries the `_` extension prefix and rejects
/// bare custom methods with `-32601 method_not_found`. These injections were
/// historically sent un-prefixed, so every watcher-driven hot-reload
/// (models, skills, MCP servers) was silently rejected at decode — the
/// watcher-side "change detected" logs fired but the reload handlers never
/// ran. Keep `method` here as the un-prefixed name; the prefix is a wire
/// detail added in one place.
/// bare custom methods with `-32601 method_not_found`. Without the prefix
/// every watcher-driven hot-reload (models, skills, MCP servers) is silently
/// rejected at decode — the watcher-side "change detected" logs fire but the
/// reload handlers never run. Keep `method` here as the un-prefixed name; the
/// prefix is a wire detail applied in one place.
fn internal_reload_request_line(id: &str, method: &str, params: serde_json::Value) -> String {
let msg = serde_json::json!({
"jsonrpc": "2.0",
@@ -419,11 +409,6 @@ pub async fn run_stdio_agent(
/// error — login is deferred to ACP.
/// 7. `ready_tx.send(true)` — unblocks ACP forwarding in the IPC server.
/// 8. LocalSet: agent, IPC↔agent bridges, config watcher.
///
/// # Arguments
///
/// * `agent_config` - The agent configuration
/// * `no_exit_on_disconnect` - If true, the leader will not exit when all clients disconnect
pub async fn run_leader(
agent_config: &AgentConfig,
no_exit_on_disconnect: bool,
@@ -476,11 +461,11 @@ pub async fn run_leader(
Err(e) => return Err(anyhow::anyhow!("Failed to check leader lock: {}", e)),
};
// ── Phase 1: Clean up stale socket ────────────────────────────────────────
// Phase 1: Clean up stale socket
lock.cleanup_socket()?;
info!("Leader server starting");
// ── Phase 2: Create all channels + readiness watch ────────────────────────
// Phase 2: Create all channels + readiness watch
//
// All channels are created here so the IPC server can start receiving
// client connections immediately, before auth/prefetch begin.
@@ -526,7 +511,7 @@ pub async fn run_leader(
leader_binary_version: kigi_version::VERSION.to_string(),
});
// ── Phase 3: Bind socket and start IPC server (BEFORE auth/prefetch) ──────
// Phase 3: Bind socket and start IPC server (BEFORE auth/prefetch)
//
// Starting the server here means connect_or_spawn sees the socket in < 100 ms
// regardless of how long auth + model prefetch take. The `ready_rx` gate inside
@@ -550,7 +535,8 @@ pub async fn run_leader(
agent_activity_for_server,
ready_rx,
shutdown_tx_for_server,
None, // use LEADER_VERSION constant
// None: use the LEADER_VERSION constant.
None,
control_state,
)
.await
@@ -559,7 +545,7 @@ pub async fn run_leader(
}
});
// ── Phase 4: Wait for socket to appear (fast: < 100 ms now) ──────────────
// Phase 4: Wait for socket to appear (fast: < 100 ms)
let socket_ready_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while !crate::leader::listener_is_ready(&socket_path) {
if tokio::time::Instant::now() >= socket_ready_deadline {
@@ -572,7 +558,7 @@ pub async fn run_leader(
}
debug!("IPC socket created");
// ── Phase 5: Lock handoff ─────────────────────────────────────────────────
// Phase 5: Lock handoff
//
// (a) lock_already_held=true: We acquired the lock at startup. Keep it.
// (b) lock_already_held=false: spawner holds lock, waiting for our socket.
@@ -609,7 +595,7 @@ pub async fn run_leader(
}
};
// ── Phase 6: Auth + model prefetch ───────────────────────────────────────
// Phase 6: Auth + model prefetch
//
// The IPC server is already accepting connections. Clients that send ACP
// messages during this window receive a `leader_starting` error and can retry.
@@ -635,14 +621,14 @@ pub async fn run_leader(
.await
.unwrap_or(None);
// ── Phase 7: Signal readiness ─────────────────────────────────────────────
// Phase 7: Signal readiness
//
// Unblocks ACP forwarding inside the IPC server. From this point on, client
// ACP messages are forwarded to the agent as normal.
let _ = ready_tx.send(true);
info!("Leader ready: auth and model prefetch complete, ACP forwarding enabled");
// ── Phase 8: LocalSet — agent, bridges, config watcher ───────────────────
// Phase 8: LocalSet — agent, bridges, config watcher
let local_set = tokio::task::LocalSet::new();
let mut agent_config_for_spawn = agent_config.clone();
@@ -672,13 +658,10 @@ pub async fn run_leader(
let models_manager_for_agent = shared_models_manager.clone();
let models_manager_for_config = shared_models_manager;
// Resolve `mcp.recursive_config_watch`
// ONCE here, before the channel is created, so a kill-switch
// value of `false` skips channel construction entirely. Previously
// the channel was always created and `tx` always installed on
// the agent; the drain task only ran when the flag was on, so
// every `notify_session_cwd_for_watch` call leaked a `PathBuf`
// into a never-drained channel.
// Resolve `mcp.recursive_config_watch` once here, before the channel is
// created, so a kill-switch value of `false` skips channel construction
// entirely. Otherwise every `notify_session_cwd_for_watch` call would leak
// a `PathBuf` into a channel whose drain task never runs.
let recursive_config_watch_enabled = {
let user_cfg = crate::config::load_from_disk().ok();
let requirements = crate::agent::config::read_requirements_toml();
@@ -691,19 +674,17 @@ pub async fn run_leader(
local_set
.run_until(async move {
// Channel for fanning new session cwds from
// the agent (each `spawn_and_register_session` call) into
// the leader's `ConfigFileWatcher::watch_path`. Both ends
// live inside the `LocalSet` so neither needs `Send`. The
// tx is installed on the agent before `AgentSideConnection`
// moves it; the rx is drained by a small task spawned
// alongside the watcher below.
// Channel for fanning new session cwds from the agent (each
// `spawn_and_register_session` call) into the leader's
// `ConfigFileWatcher::watch_path`. Both ends live inside the
// `LocalSet` so neither needs `Send`. The tx is installed on the
// agent before `AgentSideConnection` moves it; the rx is drained by
// a small task spawned alongside the watcher below.
//
// Only create the channel when the kill-
// switch is `true`. With the flag off,
// `notify_session_cwd_for_watch` becomes a no-op (no
// `tx` installed) and no memory leaks regardless of how
// many sessions spawn over the leader's lifetime.
// Only create the channel when the kill-switch is `true`. With the
// flag off, `notify_session_cwd_for_watch` becomes a no-op (no `tx`
// installed) and no memory leaks regardless of how many sessions
// spawn over the leader's lifetime.
let (config_watcher_path_tx, config_watcher_path_rx_opt) =
if recursive_config_watch_enabled {
let (tx, rx) = mpsc::unbounded_channel::<std::path::PathBuf>();
@@ -823,12 +804,10 @@ pub async fn run_leader(
let (config_update_tx, mut config_update_rx) =
mpsc::unbounded_channel::<crate::config::reloader::ConfigUpdate>();
// `mcp.recursive_config_watch` (default
// `true`) was resolved above (before the async block) so
// the per-session-cwd channel could be gated. The
// watcher passes `Some(cwd)` here only when the flag is
// on. When disabled, behavior reverts to the prior
// default: only explicit `extra_paths` are watched (kill
// `mcp.recursive_config_watch` (default `true`) was resolved above,
// before the async block, so the per-session-cwd channel could be
// gated. The watcher passes `Some(cwd)` here only when the flag is
// on; when disabled, only explicit `extra_paths` are watched (kill
// switch for the rollout).
let watcher_cwd = recursive_config_watch_enabled.then_some(cwd_for_watcher.as_path());
@@ -839,19 +818,16 @@ pub async fn run_leader(
watcher_cwd,
None,
) {
// Share ownership between the leader's
// long-lived binding and the per-cwd dynamic
// registration drain task. `Rc<RefCell<>>` is safe
// because both ends live inside the leader's
// `LocalSet` — the watcher type is not `Sync`-needed.
// Share ownership between the leader's long-lived binding and
// the per-cwd dynamic registration drain task. `Rc<RefCell<>>`
// is safe because both ends live inside the leader's `LocalSet`
// — the watcher type is not `Sync`-needed.
let watcher = std::rc::Rc::new(std::cell::RefCell::new(watcher));
// Dynamic registration drain. Lives only
// when the recursive_config_watch flag is on AND the
// OS watcher started. With the flag
// off the channel itself was never created, so
// there's no rx to drain and no `PathBuf` ever
// queued (no leak).
// Dynamic registration drain. Lives only when the
// recursive_config_watch flag is on AND the OS watcher started.
// With the flag off the channel itself is never created, so
// there's no rx to drain and no `PathBuf` ever queued (no leak).
if let Some(mut rx) = config_watcher_path_rx.take() {
let cancel_for_drain = cancel_clone.clone();
let watcher_for_drain = watcher.clone();
@@ -945,14 +921,11 @@ pub async fn run_leader(
}
}
ConfigUpdate::ProjectMcpServersChanged { cwd } => {
// Scope the reload to
// sessions whose cwd matches `cwd` (or is
// a descendant). The actual filtering
// happens in
// `handle_reload_project_mcp_servers`
// (extensions/session_admin.rs) — this
// arm just injects the ACP method with
// the cwd as a param.
// Scope the reload to sessions whose cwd matches
// `cwd` (or is a descendant). The actual filtering
// happens in `handle_reload_project_mcp_servers`
// (extensions/session_admin.rs) — this arm just
// injects the ACP method with the cwd as a param.
info!(
cwd = %cwd.display(),
"project MCP config change detected — reloading matching sessions"
@@ -1170,10 +1143,12 @@ mod tests {
#[tokio::test]
async fn auto_update_defers_when_agent_busy() {
let agent_busy = Arc::new(AtomicBool::new(true)); // agent is processing a prompt
// agent is processing a prompt
let agent_busy = Arc::new(AtomicBool::new(true));
let cancel = CancellationToken::new();
let config = delayed_update_config(0); // always returns true
// always returns true
let config = delayed_update_config(0);
let cancel_clone = cancel.clone();
let checker = tokio::spawn(run_auto_update_checker(
@@ -1229,7 +1204,8 @@ mod tests {
#[tokio::test]
async fn auto_update_cancels_after_agent_becomes_idle() {
let agent_busy = Arc::new(AtomicBool::new(true)); // agent processing initially
// agent processing initially
let agent_busy = Arc::new(AtomicBool::new(true));
let cancel = CancellationToken::new();
// Update is always available, but agent is busy initially
@@ -1299,7 +1275,8 @@ mod tests {
let call_count = Arc::new(AtomicU32::new(0));
let call_count_clone = call_count.clone();
let agent_busy = Arc::new(AtomicBool::new(true)); // agent busy, so it defers
// agent busy, so it defers
let agent_busy = Arc::new(AtomicBool::new(true));
let cancel = CancellationToken::new();
let config = LeaderAutoUpdateConfig {
@@ -1308,7 +1285,8 @@ mod tests {
let cc = call_count_clone.clone();
Box::pin(async move {
cc.fetch_add(1, Ordering::Relaxed);
true // update always available, but won't cancel because agent is busy
// update always available, but won't cancel because agent is busy
true
})
}),
};
@@ -1381,13 +1359,15 @@ mod tests {
/// pending interaction, or live subagent).
#[tokio::test]
async fn auto_update_defers_when_agent_activity_busy() {
let agent_busy = Arc::new(AtomicBool::new(false)); // IPC view: idle
// IPC view: idle
let agent_busy = Arc::new(AtomicBool::new(false));
let activity = crate::agent::activity::AgentActivity::default();
// Agent view: a subagent is running (e.g. spawned by a relay prompt).
activity.subagent_gauge().store(1, Ordering::Relaxed);
let cancel = CancellationToken::new();
let config = always_config(true); // update always "installed"
// update always "installed"
let config = always_config(true);
let cancel_clone = cancel.clone();
let checker = tokio::spawn(run_auto_update_checker(
@@ -1423,7 +1403,8 @@ mod tests {
activity.subagent_gauge().store(1, Ordering::Relaxed);
let cancel = CancellationToken::new();
let config = always_config(true); // update always "installed"
// update always "installed"
let config = always_config(true);
// 10ms interval × (24 deferrals + 1) ≈ 250ms — well within timeout.
tokio::time::timeout(
@@ -1500,7 +1481,8 @@ mod tests {
let cancel = CancellationToken::new();
let (shutdown_tx, mut shutdown_rx) = watch::channel(crate::leader::ShutdownReason::Manual);
let config = always_config(true); // update always available
// update always available
let config = always_config(true);
tokio::time::timeout(
Duration::from_secs(2),
@@ -1517,8 +1499,9 @@ mod tests {
assert!(cancel.is_cancelled(), "cancel token should be triggered");
// The shutdown_tx must have been updated to AutoUpdate before cancel fired.
shutdown_rx.mark_changed(); // ensure borrow sees latest value
// The shutdown_tx must carry AutoUpdate before the cancel fired.
// ensure borrow sees latest value
shutdown_rx.mark_changed();
assert_eq!(
*shutdown_rx.borrow(),
crate::leader::ShutdownReason::AutoUpdate,
@@ -13,7 +13,7 @@ use crate::auth::credential_authority::CredentialClass;
/// cell is correct.
pub(crate) type SharedAuthMethodId = std::sync::Arc<arc_swap::ArcSwapOption<acp::AuthMethodId>>;
/// Construct a [`SharedAuthMethodId`]. `None` is the pre-`authenticate` state.
/// `None` is the pre-`authenticate` state.
pub(crate) fn new_shared_auth_method_id(initial: Option<acp::AuthMethodId>) -> SharedAuthMethodId {
std::sync::Arc::new(arc_swap::ArcSwapOption::new(
initial.map(std::sync::Arc::new),
@@ -21,17 +21,15 @@ pub(crate) fn new_shared_auth_method_id(initial: Option<acp::AuthMethodId>) -> S
}
/// Primary env var that, when set, advertises `xai.api_key` as a viable auth
/// method. NOTE: `xai.api_key` is the *house* bring-your-own-key method (the
/// upstream product is house-branded "xai"), unrelated to the x.ai/Grok
/// provider. That collision is why the primary env moved here to `KIGI_API_KEY`
/// — `XAI_API_KEY` is now the x.ai/Grok provider key (see `XAI_SPEC`).
///
/// Kept as a constant so test code and the production check stay in sync.
/// method. `xai.api_key` is the *house* bring-your-own-key method (the upstream
/// product is house-branded "xai"), unrelated to the x.ai/Grok provider — that
/// name collision is why the house primary env is `KIGI_API_KEY` while
/// `XAI_API_KEY` is the x.ai/Grok provider key (see `XAI_SPEC`).
pub const HOUSE_API_KEY_ENV_VAR: &str = "KIGI_API_KEY";
/// Back-compat fallback env: `XAI_API_KEY` was the house BYOK key before it
/// became the x.ai/Grok provider key. Still honored so existing house-BYOK
/// deployments keep working (they share the key with the Grok provider).
/// Back-compat fallback env, also honored as a house BYOK key so existing
/// house-BYOK deployments keep working — they share `XAI_API_KEY` with the
/// x.ai/Grok provider.
pub const XAI_API_KEY_ENV_VAR: &str = "XAI_API_KEY";
/// Legacy env var name (pre-`XAI_API_KEY`). Checked last so the oldest
@@ -39,17 +37,13 @@ pub const XAI_API_KEY_ENV_VAR: &str = "XAI_API_KEY";
pub const LEGACY_XAI_API_KEY_ENV_VAR: &str = "KIGI_CODE_XAI_API_KEY";
/// Read the house BYOK API key from the environment.
///
/// Checks `KIGI_API_KEY` first, then the back-compat `XAI_API_KEY`, then the
/// legacy `KIGI_CODE_XAI_API_KEY`.
pub fn read_xai_api_key_env() -> Result<String, std::env::VarError> {
std::env::var(HOUSE_API_KEY_ENV_VAR)
.or_else(|_| std::env::var(XAI_API_KEY_ENV_VAR))
.or_else(|_| std::env::var(LEGACY_XAI_API_KEY_ENV_VAR))
}
/// Returns `true` if any house BYOK env is set: `KIGI_API_KEY` (primary) or the
/// back-compat `XAI_API_KEY` / `KIGI_CODE_XAI_API_KEY`.
/// Whether any house BYOK env var is set.
pub fn has_xai_api_key_env() -> bool {
read_xai_api_key_env().is_ok()
}
@@ -58,8 +52,9 @@ pub fn has_xai_api_key_env() -> bool {
/// the `auth_methods` list at `initialize()` time.
///
/// Regression: `xai.api_key` must stay first when only per-model credentials
/// exist (no global `XAI_API_KEY`). Deferring it made BYOK users hit the login
/// screen because the pager uses `auth_methods.first()` for startup metadata.
/// exist (no global `XAI_API_KEY`). Deferring it past `first()` sends BYOK
/// users to the login screen, since the pager reads `auth_methods.first()` for
/// startup metadata.
///
/// [`build_auth_methods`] consumes this predicate and pins the ordering;
/// its tests catch call-site and predicate regressions.
@@ -107,9 +102,9 @@ pub struct BuiltAuthMethods {
/// pre-computed inputs.
///
/// REGRESSION GUARD: when `has_external_api_key` is true, the **first** entry
/// MUST be `xai.api_key`. A prior change deferred it to the END for per-model
/// credentials, which made the pager send per-model-key users to the login
/// screen. Unit tests lock this.
/// MUST be `xai.api_key`. If it is deferred past `first()`, the pager sends
/// per-model-key users to the login screen (it reads `auth_methods.first()`
/// for startup metadata). Unit tests lock this.
///
/// Ordering (when each method is enabled):
/// 1. `xai.api_key` (if `has_external_api_key`)
@@ -207,9 +202,7 @@ impl AuthMethodKind {
CACHED_TOKEN_AUTH_METHOD_ID => Self::CachedToken,
KIMI_CODE_METHOD_ID => Self::KimiCode,
other => match kigi_models::PlatformId::parse(other) {
// A generic device-code OAuth platform (xai-grok).
Some(p) if p.oauth().is_some() => Self::OAuthPlatform(p),
// A non-OAuth API-key registry platform.
Some(p) if !p.uses_oauth() => Self::ApiKeyPlatform(p),
_ => Self::Unknown,
},
@@ -377,9 +370,8 @@ pub fn cached_token_auth_method() -> acp::AuthMethod {
)
}
/// Interactive login method id, advertised over ACP by this agent and
/// selected by the in-repo pager. Both sides of the ACP boundary live in
/// this repo, so the id is renamed in lockstep everywhere.
/// Interactive login method id, advertised over ACP by this agent and matched
/// by the in-repo pager — both sides of the ACP boundary share this constant.
pub const KIMI_CODE_METHOD_ID: &str = "kimi-code";
/// The Kimi Code device-code login.
@@ -1115,8 +1107,8 @@ mod tests {
assert_eq!(read_xai_api_key_env().unwrap(), "new-key");
}
/// After the migration, `KIGI_API_KEY` is the house BYOK primary and wins
/// over the back-compat `XAI_API_KEY` (now the x.ai/Grok provider key).
/// `KIGI_API_KEY` is the house BYOK primary and wins over the back-compat
/// `XAI_API_KEY` (the x.ai/Grok provider key).
#[test]
#[serial]
fn house_env_var_takes_precedence_over_xai() {
@@ -1,16 +1,15 @@
//! Legacy `--chat` gateway gate.
//!
//! The kimi.com chat-product model picker (`/rest/modes`, `ChatModesManager`)
//! was removed with the xAI proxy: those "modes" came from a kigi backend with
//! no Kimi counterpart. Only the process-mode gate survives so the `--chat`
//! frontend path stays a compile-time-off no-op across crates without a
//! has no Kimi counterpart, so only this process-mode gate remains: it keeps
//! the `--chat` frontend path a compile-time-off no-op across crates without a
//! cross-crate churn to delete every reference.
/// Process-wide flag set by the pager when started with `--chat`.
pub const KIGI_CHAT_MODE_ENV: &str = "KIGI_CHAT_MODE";
/// True when the process is a gateway light-frontend (`--chat`) agent.
/// Hard-off: the kigi chat-modes backend is gone, so this is always `false`.
/// Whether the process is a gateway light-frontend (`--chat`) agent. Always
/// `false`: the kigi chat-modes backend has no Kimi counterpart.
pub fn process_chat_mode_enabled() -> bool {
false
}
+15 -17
View File
@@ -994,7 +994,7 @@ pub struct StorageConfig {
///
/// These supplement the built-in scan locations (`.kigi/skills/`,
/// `.agents/skills/`, `~/.kigi/skills/`). They're written by `/import-claude`
/// to preserve previously-discovered Claude directories after the runtime
/// to preserve already-discovered Claude directories after the runtime
/// `.claude/` cutoff (see `[claude_compat] imported`).
///
/// Example:
@@ -4015,28 +4015,17 @@ pub(crate) fn resolve_aux_model_sampling_config(
);
None
}
/// Finalize image-describe model + sampler config for user attachments.
/// Shared so the aux resolve happy path and the
/// `None` fallback cannot diverge between those entry points.
///
/// On aux resolve `Some`, stamp session-local fields (attribution, bearer,
/// retries) onto the helper config. On `None`, fall back to the active session model and
/// full config (not forcing `image_description_model` onto the agent endpoint, which 404s
/// on BYOK / non-proxy routes for internal slugs).
/// Stamp the session-local fields (attribution, bearer resolver, retries)
/// from the active session onto a routed aux `SamplerConfig` so a
/// helper model keeps the session's auth/attribution. Shared by image-describe
/// and the auto-mode classifier so the two can't drift.
/// Stamp the session-local fields onto a routed aux `SamplerConfig`.
///
/// `bearer_resolver` is an EXPLICIT parameter, not a copy of the session's:
/// this helper used to clone `active_session_config.bearer_resolver`
/// unconditionally and rely on every call site remembering to re-point it
/// afterwards. `SamplingClient::post` REPLACES the request's auth header from
/// the resolver, so a forgotten re-point overwrote the aux model's own key on
/// the AUX host with the session bearer. Callers obtain the value from
/// `SamplingClient::post` REPLACES the request's auth header from the
/// resolver, so handing it the session bearer would overwrite the aux
/// model's own key on the AUX host. Callers obtain the value from
/// [`SessionActor::aux_bearer_resolver`](crate::session::acp_session::SessionActor)
/// — the chokepoint — so "forgot to re-point" is no longer expressible.
/// — the chokepoint that keeps the two hosts' credentials from crossing.
pub(crate) fn stamp_session_local_sampler_fields(
cfg: &mut SamplerConfig,
active_session_config: &SamplerConfig,
@@ -4047,6 +4036,14 @@ pub(crate) fn stamp_session_local_sampler_fields(
cfg.bearer_resolver = bearer_resolver;
cfg.max_retries = max_retries;
}
/// Finalize image-describe model + sampler config for user attachments.
/// Shared so the aux resolve happy path and the
/// `None` fallback cannot diverge between those entry points.
///
/// On aux resolve `Some`, stamp session-local fields (attribution, bearer,
/// retries) onto the helper config. On `None`, fall back to the active session model and
/// full config (not forcing `image_description_model` onto the agent endpoint, which 404s
/// on BYOK / non-proxy routes for internal slugs).
pub(crate) fn finalize_image_describe_sampler_config(
resolved_aux: Option<SamplerConfig>,
active_session_config: &SamplerConfig,
@@ -5985,7 +5982,8 @@ reasoning_effort = "low"
fn byok_custom_entries_default_to_passthrough_except_house_endpoint() {
let make_cfg = |base_url: &str| {
let entry_cfg = ModelEntryConfig {
id: None, // BYOK: no managed platform key
// BYOK: no managed platform key
id: None,
model: "my-custom-model".to_string(),
base_url: base_url.to_string(),
name: None,
@@ -17,7 +17,6 @@ use serde::Serialize;
use super::config::ConfigModelOverride;
/// Category for a [`ModelOverrideWarning`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ModelOverrideWarningKind {
@@ -48,7 +47,6 @@ pub struct ModelOverrideWarning {
pub reason: String,
}
/// Result of [`parse_model_overrides`].
pub(crate) struct ParsedModelOverrides {
pub models: IndexMap<String, ConfigModelOverride>,
pub warnings: Vec<ModelOverrideWarning>,
@@ -44,12 +44,10 @@ fn current_keep_set() -> Vec<String> {
.collect()
}
/// Whether any of `platforms` needs enrichment at all.
pub(crate) fn any_platform_needs_enrichment(platforms: &[kigi_models::PlatformId]) -> bool {
platforms.iter().any(|p| !p.wire_serves_metadata())
}
/// The registry's models.dev provider ids (the refresh filter).
fn registry_models_dev_ids() -> BTreeSet<&'static str> {
kigi_models::PlatformId::ALL
.into_iter()
@@ -206,7 +204,7 @@ mod tests {
/// Wire-served-only platform sets never trigger IO — and never force the
/// bundled parse (empty owned catalog; the merge branch is gated off).
/// Kimi/Moonshot users therefore keep a zero-egress, zero-cache fetch
/// path even now that enrichment-needing platforms (OpenAI) exist.
/// path even though enrichment-needing platforms (OpenAI) exist.
#[test]
fn wire_served_platforms_get_empty_catalog_without_io() {
let wire_served = [
@@ -217,7 +215,7 @@ mod tests {
assert!(!any_platform_needs_enrichment(&wire_served));
let catalog = load_enrichment_catalog(&wire_served);
assert!(catalog.is_empty());
// The full registry now DOES need enrichment (OpenAI is
// The full registry DOES need enrichment (OpenAI is
// wire_serves_metadata=false) — the fast path must not hide that.
assert!(any_platform_needs_enrichment(&kigi_models::PlatformId::ALL));
}
@@ -24,7 +24,6 @@ pub struct FeedbackApiError {
}
impl FeedbackApiError {
/// Returns `true` if this is a 401 Unauthorized response.
pub fn is_unauthorized(&self) -> bool {
self.status == reqwest::StatusCode::UNAUTHORIZED
}
@@ -49,7 +48,6 @@ enum BearerSource {
Static(String),
}
/// Client for the Kimi Code feedback endpoint.
#[derive(Clone)]
pub struct FeedbackClient {
http: reqwest::Client,
@@ -73,7 +71,6 @@ impl FeedbackClient {
}
}
/// Client with a fixed Bearer token (tests).
#[cfg(test)]
pub(crate) fn with_static_token(base_url: impl Into<String>, token: impl Into<String>) -> Self {
Self {
@@ -31,8 +31,8 @@ use agent_client_protocol as acp;
use kigi_workspace::trust::{TrustStore, is_unsafe_trust_root, workspace_key};
use parking_lot::Mutex;
// Decision-side (scan/decide/prompt/store) relocated to `kigi-workspace`
// (client crate). `grant_folder_trust` is the ONLY moved item referenced from
// The decision-side (scan/decide/prompt/store) lives in `kigi-workspace`
// (client crate). `grant_folder_trust` is the ONLY item referenced from
// OUTSIDE this module (shell call sites + the pager's
// `kigi_shell::agent::folder_trust::grant_folder_trust`), so only it is
// re-published; the rest are private imports used within this module. A glob
@@ -50,11 +50,11 @@ use crate::session::managed_mcp::mcp_server_name;
use crate::util::config::{MCP_SCOPE_PROJECT, RemoteSettings};
// NOTE: this folder-trust store (`~/.kigi/trusted_folders.toml`) is SEPARATE
// from the pre-existing per-plugin trust store
// from the per-plugin trust store
// (`kigi_agent::plugins::TrustStore` at `~/.kigi/trusted-plugins`, plus the
// hooks' own project-trust gating). Trusting a folder here does NOT imply plugin
// trust and vice versa; the two are independent and non-contradicting.
// Unifying them is a tracked follow-up (out of scope for this PR).
// Unifying them is a tracked follow-up.
/// Per-workspace resolved decision: `true` = repo-local (project-scoped)
/// servers are allowed to spawn. Keyed by canonical workspace key.
@@ -788,7 +788,7 @@ mod tests {
)
.unwrap();
// Detection now walks cwd→root, so the subdir-only `.claude` is detected
// Detection walks cwd→root, so the subdir-only `.claude` is detected
// and the folder resolves untrusted.
assert!(
repo_configs_present(&subdir),
@@ -938,7 +938,8 @@ mod tests {
// KIGI_TEST_VERSION unset so `is_local_build()` is genuinely true.
let _unset_ver = EnvGuard::unset(kigi_version::TEST_VERSION_ENV);
if option_env!("KIGI_VERSION").is_some() {
return; // a release-stamped test binary is not a local build
// a release-stamped test binary is not a local build
return;
}
let home = tempfile::tempdir().unwrap();
let _env = EnvGuard::set("KIGI_SHARE_DIR", home.path());
@@ -1507,7 +1508,8 @@ mod tests {
// real store is never touched.
let _sim = EnvGuard::unset(kigi_version::TEST_VERSION_ENV);
if option_env!("KIGI_VERSION").is_some() {
return; // a release-stamped test binary is not a local build
// a release-stamped test binary is not a local build
return;
}
let home = tempfile::tempdir().unwrap();
let _env = EnvGuard::set("KIGI_SHARE_DIR", home.path());
@@ -1,7 +1,4 @@
//! Session meta-information handlers.
//!
//! Router pattern: single `handle()` dispatches by method name.
//! Business logic delegates to pure functions or MvpAgent methods.
use std::collections::BTreeMap;
use std::path::PathBuf;
@@ -28,7 +25,6 @@ fn backfill_session_summary(summary: &mut Summary) {
}
}
/// Router for kigi/session/* and kigi/session_summaries/* methods.
pub async fn handle(
agent: &MvpAgent,
args: &acp::ExtRequest,
@@ -105,7 +101,6 @@ async fn handle_session_info(
.send(SessionCommand::GetSessionInfo { responds_to: tx });
let info = rx.await.ok();
// Construct display data for `/session-info`.
let mut data = info.unwrap_or_else(|| SessionInfoData {
agent_name: None,
model: None,
@@ -124,21 +119,18 @@ async fn handle_session_info(
},
});
// Calculate the model's display name.
data.model_display_name = agent
.models_manager
.models()
.get(session.model_id.0.as_ref())
.and_then(|entry| entry.info.name.clone());
// Construct `SessionInfoResponse`.
let response = SessionInfoResponse {
session_id,
cwd: session.info.cwd.clone(),
data,
};
// Wrap `SessionInfoResponse` in `ExtMethodResult` and return it.
ExtMethodResult::success(serde_json::to_value(&response).unwrap_or_default())
.to_ext_response()
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
@@ -239,7 +231,6 @@ async fn handle_session_summaries(
}
}
/// Group summaries by cwd and serialize into an [`AllSessionOverviewResponse`].
fn summaries_to_overview_response(summaries: Vec<Summary>) -> Result<acp::ExtResponse, acp::Error> {
let mut by_cwd: BTreeMap<String, Vec<Summary>> = Default::default();
for mut s in summaries {
@@ -259,7 +250,6 @@ fn summaries_to_overview_response(summaries: Vec<Summary>) -> Result<acp::ExtRes
Ok(acp::ExtResponse::new(value))
}
// ── Merged session list (local + remote) ─────────────────────────────
async fn handle_session_list(
agent: &MvpAgent,
@@ -1,7 +1,4 @@
//! Agent bootstrap and lifecycle hooks.
//!
//! [`bootstrap`] runs the full init sequence (config resolution, process
//! singletons, model catalog) and returns a resolved config + `ModelsManager`.
use std::sync::Arc;
@@ -12,8 +9,6 @@ use crate::agent::models::ModelsManager;
use crate::auth::AuthManager;
use crate::config::StorageMode;
/// Resolve config, init process singletons, build the model catalog.
///
/// The `ModelsManager` is `Clone + Send`, so callers that need a handle
/// for the config watcher can clone it before passing it to
/// `MvpAgent::with_models`.
@@ -47,8 +42,6 @@ pub(crate) fn exit_on_config_error<T>(e: String) -> T {
std::process::exit(1);
}
/// Config transform: apply managed settings, fetch remote settings,
/// resolve storage mode.
fn resolve_config(cfg: &AgentConfig, auth_manager: &AuthManager) -> AgentConfig {
let mut cfg = cfg.clone();
@@ -99,7 +92,6 @@ fn resolve_config(cfg: &AgentConfig, auth_manager: &AuthManager) -> AgentConfig
cfg
}
/// Initialize process-level singletons (deployment sync, bundled files).
/// `Once`-guarded: only the first call takes effect.
fn init_process(cfg: &AgentConfig, auth_manager: &AuthManager) {
use std::sync::Once;
+15 -69
View File
@@ -29,10 +29,7 @@ pub struct BaselineSamplingConfig {
pub platform: Option<kigi_models::PlatformId>,
}
// ── Auth method for model fetching ──────────────────────────────────────────
/// How the model catalog is fetched (PRD F4). The old xAI tier-gated proxy
/// fetch is gone; there are exactly two shapes now.
/// How the model catalog is fetched (PRD F4).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ModelFetchAuth {
/// Fixed platform registry: `kimi-code` via the F1 OAuth bearer plus the
@@ -44,7 +41,6 @@ pub(crate) enum ModelFetchAuth {
}
impl ModelFetchAuth {
/// Custom endpoint when configured, else the platform registry.
pub(crate) fn resolve(endpoints: &config::EndpointsConfig) -> Self {
if endpoints.has_custom_endpoint() {
Self::CustomEndpoint
@@ -214,7 +210,6 @@ struct Inner {
/// time) or `reselect_current_model_if_missing` (subsequent).
/// Reset in `clear()` for identity changes.
has_fetched_real_catalog: RwLock<bool>,
// ── Owned context for self-contained refresh ────────────────
auth_manager: Arc<AuthManager>,
cfg: RwLock<config::Config>,
fetch_auth: RwLock<ModelFetchAuth>,
@@ -241,10 +236,8 @@ struct Inner {
/// `maybe_fire_laziness_check`'s polling loop to detect a
/// switch that occurred during the idle wait or sampler call.
///
/// `watch::Sender` natively fans out to every subscriber, so this
/// replaces the previous `RwLock<Vec<Arc<Notify>>>` listener
/// registry — no manual fan-out, no listener-leak risk, no
/// `unregister` API to maintain.
/// `watch::Sender` natively fans out to every subscriber — no manual
/// fan-out, no listener-leak risk, no `unregister` API to maintain.
model_switch_watch: tokio::sync::watch::Sender<u64>,
}
@@ -295,8 +288,7 @@ impl ModelsManager {
/// Subscribe to model-switch events. Returns a `watch::Receiver`
/// carrying the monotonic generation counter. `.changed()` only
/// resolves on switches that occur **after** subscription, so
/// there is no stored-permit hazard (the bug that motivated
/// replacing the previous `Arc<Notify>` design).
/// there is no stored-permit hazard.
pub fn subscribe_model_switch(&self) -> tokio::sync::watch::Receiver<u64> {
self.inner.model_switch_watch.subscribe()
}
@@ -469,8 +461,6 @@ impl ModelsManager {
self.notify_models_updated();
}
// ── Accessors ───────────────────────────────────────────────────
pub fn models(&self) -> IndexMap<String, ModelEntry> {
self.inner.models.read().clone()
}
@@ -704,8 +694,6 @@ impl ModelsManager {
*self.inner.has_fetched_real_catalog.read()
}
// ── Mutations ───────────────────────────────────────────────────
fn rebuild(&self, cfg: &config::Config, prefetched: Option<IndexMap<String, ModelEntry>>) {
*self.inner.models.write() =
resolve_model_catalog(cfg, prefetched, &PlatformApiKeys::resolve(&cfg.platforms));
@@ -779,7 +767,8 @@ impl ModelsManager {
// Deliberate no-fetch state, not a failure: no warn-class log.
tracing::debug!("model catalog: bundled defaults in use (remote_fetch disabled)");
}
self.rebuild(&config, None); // first-time only: no fetched catalog, use bundled defaults
// first-time only: no fetched catalog, use bundled defaults
self.rebuild(&config, None);
self.reselect_current_model_if_missing(&config);
// Schedule background retries so we recover once the network is
@@ -794,7 +783,6 @@ impl ModelsManager {
self.notify_models_updated();
}
/// Notify clients about the current model catalog.
fn notify_models_updated(&self) {
let available = self.available();
let current = self.current_model_id();
@@ -899,7 +887,7 @@ impl ModelsManager {
}
// Recompute the prompt-block flag (mirrors `apply_refresh_result`) so
// a corrective external cache write unlatches a previously latched
// a corrective external cache write unlatches an
// "allowlist excludes everything" state instead of keeping prompts
// blocked against a stale catalog.
let excludes_all = allowlist_matches_nothing(&cfg, &self.inner.models.read());
@@ -1111,9 +1099,9 @@ impl ModelsManager {
};
// H1: the session bearer comes from the ONE credential chokepoint,
// resolved against the CURRENT MODEL's own platform AND endpoint. This
// used to be a local re-derivation that fell through to
// `auth_manager.current_or_expired()` for every non-OAuth platform —
// resolved against the CURRENT MODEL's own platform AND endpoint. A
// local re-derivation that fell through to
// `auth_manager.current_or_expired()` for every non-OAuth platform is
// byte-for-byte the round-1 defect, reachable with ZERO configuration
// (`default_models.json` bundles `moonshot-cn/*` entries a Kimi
// subscription user sees on first launch / offline, and this config is
@@ -1448,8 +1436,6 @@ impl ModelsManager {
}
}
// ── Refresh strategy ────────────────────────────────────────────────────────
/// How to resolve the model list.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefreshStrategy {
@@ -1461,8 +1447,6 @@ pub enum RefreshStrategy {
OnlineIfUncached,
}
// ── Disk cache ──────────────────────────────────────────────────────────────
const MODELS_CACHE_FILE: &str = "models_cache.json";
const CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(300);
@@ -1650,7 +1634,7 @@ impl ModelsCacheManager {
/// Sync; see `load_fresh` note. Best-effort, but NEVER silent: a failed
/// cache write leaves a stale catalog on disk, which on Windows (sharing
/// violations) previously diverged picker behavior with zero trace.
/// violations) diverged picker behavior with zero trace.
fn atomic_write(&self, cache: &ModelsCache) {
if let Some(parent) = self.path.parent() {
let _ = std::fs::create_dir_all(parent);
@@ -1692,8 +1676,6 @@ impl ModelsCacheManager {
}
}
// ── Fetch ───────────────────────────────────────────────────────────────────
/// Build the prefetched model map from a flat list of entries.
///
/// Each entry is keyed by its `id` field (falling back to the `model` slug
@@ -1885,15 +1867,6 @@ fn resolve_prefetch_env_with_auth(auth: Option<KimiAuth>) -> Option<PrefetchEnv>
)
}
/// Decision core of [`resolve_prefetch_env_with_auth`], split from the config
/// loading so the gate is unit-testable.
///
/// `remote_fetch_enabled = false` wins over every credential shape AND over
/// `has_custom_endpoint()` (which otherwise forces the prefetch to run): the
/// explicit off switch must hold even when a stray login, a platform API key,
/// or a `deployment_key` would re-arm the prefetch — and with it the
/// deployment-config sync on the prefetch thread.
///
/// Decision core of [`ModelsManager::on_auth_changed`]'s wipe guard, split
/// from the disk probes so it is unit-testable: `true` = no fetch source
/// exists, wipe the previous identity's catalog.
@@ -2072,12 +2045,6 @@ pub(crate) fn resolve_catalog_key(
/// The one caller that legitimately passes `current_model_id` is the SHARED
/// `MvpAgent::sampling_config`, which `ModelsManager::sampling_config` builds
/// from exactly that key — there the two lookups must agree (H-a).
///
/// (L: this rule used to be stated on a `managed_key_for_slug` wrapper that no
/// caller needed once [`platform_for_slug`] resolved the entry itself. The
/// crate-level `#![allow(dead_code)]` in `lib.rs` means an unused helper on a
/// credential path raises no warning, so dead ones are deleted on sight rather
/// than left as a second, unexercised way to answer the same question.)
pub(crate) fn entry_for_slug<'a>(
models: &'a IndexMap<String, ModelEntry>,
current_key: Option<&str>,
@@ -2709,7 +2676,6 @@ mod tests {
#[tokio::test]
async fn refresh_if_new_etag_skips_when_same() {
let mgr = test_manager();
// Set initial etag
*mgr.inner.etag.write() = Some("\"abc123\"".to_string());
// Same etag — should be a no-op (etag stays the same)
@@ -3078,8 +3044,6 @@ mod tests {
.collect()
}
// ── auth-change refresh: has_fetched_real_catalog flag ─────────────
#[test]
fn first_apply_refresh_reselects_default_model() {
let mgr = test_manager();
@@ -3153,8 +3117,6 @@ mod tests {
);
}
// ── apply_config: honor changed preferred model from config ────────
#[test]
fn apply_config_honors_new_preferred_model() {
let mgr = test_manager();
@@ -3267,8 +3229,6 @@ mod tests {
);
}
// ── end-to-end: auth refresh + config reload compose correctly ───
#[test]
fn auth_refresh_then_config_reload_preserves_user_model() {
let mgr = test_manager();
@@ -3298,8 +3258,6 @@ mod tests {
assert_eq!(mgr.current_model_id().0.as_ref(), "kigi-4");
}
// ── disk-cache hot-reload (external models_cache.json writes) ────
fn test_cache_manager(dir: &std::path::Path) -> ModelsCacheManager {
ModelsCacheManager {
path: dir.join(MODELS_CACHE_FILE),
@@ -3539,8 +3497,6 @@ mod tests {
assert!(!mgr.models().contains_key("kigi-legacy"));
}
// ── clear() resets has_fetched_real_catalog ──────────────────────
#[test]
fn clear_resets_has_fetched_real_catalog() {
let mgr = test_manager();
@@ -3610,12 +3566,14 @@ mod tests {
let mut cfg = config::Config::default();
cfg.models.default = Some("alpha".to_string());
mgr.apply_refresh_result(&cfg, Some(make_prefetched(&["alpha", "beta"])), None);
*mgr.inner.cfg.write() = cfg.clone(); // old_preferred = "alpha"
// old_preferred = "alpha"
*mgr.inner.cfg.write() = cfg.clone();
assert_eq!(mgr.current_model_id().0.as_ref(), "alpha");
let mut new_cfg = config::Config::default();
new_cfg.models.default = Some("beta".to_string());
new_cfg.models.default_is_campaign_driven = true; // campaign overriding
// campaign overriding
new_cfg.models.default_is_campaign_driven = true;
mgr.apply_config(new_cfg);
assert_eq!(
mgr.current_model_id().0.as_ref(),
@@ -3694,8 +3652,6 @@ mod tests {
);
}
// ── ModelFetchAuth::resolve + PlatformApiKeys tests ─────────────
use kigi_test_support::EnvGuard;
use serial_test::serial;
@@ -3800,8 +3756,6 @@ mod tests {
assert!(dbg.contains("true") && dbg.contains("false"));
}
// ── remote_fetch gate: resolve_prefetch_env_from_parts ───────────
/// remote_fetch=false must return `None` against every re-arming shape at
/// once — session auth, a moonshot platform key, AND a custom models
/// endpoint (which normally forces the prefetch to run).
@@ -3942,8 +3896,6 @@ mod tests {
);
}
// ── supported_in_api tests ──────────────────────────────────────
#[test]
fn default_model_skips_oauth_only_for_api_key_users() {
let cfg = config::Config::default();
@@ -4048,8 +4000,6 @@ mod tests {
);
}
// ── duplicate model slug re-keying (A/B experiment "auto" alias) ──
fn make_entry_config(model: &str, name: Option<&str>) -> config::ModelEntryConfig {
make_entry_config_with_id(None, model, name)
}
@@ -4175,8 +4125,6 @@ mod tests {
assert!(map.contains_key("kigi"));
}
// ── persisted model id → catalog key (session resume) ─────────────
#[test]
fn resolve_catalog_key_maps_routing_slug_to_config_key() {
let mut models = IndexMap::new();
@@ -4325,8 +4273,6 @@ mod tests {
.collect()
}
// ── PRD F2/F4 wiremock suite ─────────────────────────────────────
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
@@ -4,16 +4,13 @@
//! (the subscription platform via the OAuth session, the open platforms via
//! their API keys), plus the custom-endpoint OpenAI-compatible listing path.
//!
//! This is the network surface relocated out of the deleted xAI-proxy
//! backend client (`remote/`); it talks only to the configured platform
//! model endpoints (plus the models.dev metadata refresh when an enabled
//! platform needs enrichment — see `enrichment_fetch`), never to a proxy
//! backend.
//! It talks only to the configured platform model endpoints (plus the
//! models.dev metadata refresh when an enabled platform needs enrichment —
//! see `enrichment_fetch`), never to a proxy backend.
use crate::auth::KimiAuth;
use indexmap::IndexMap;
use serde::Deserialize;
/// Errors from a model-catalog fetch.
#[derive(Debug, thiserror::Error)]
pub(crate) enum BackendError {
#[error("Network error: {0}")]
@@ -571,12 +568,6 @@ fn fetch_one_platform_models(
.collect();
Ok((models, etag))
}
/// Map one F4 wire model to a catalog entry config.
///
/// SECURITY: the entry carries only env-var NAMES (`env_key`) for the open
/// platforms — never key values — because raw fetched entries are persisted
/// to the models disk cache. Config-file keys are stamped in-memory later by
/// `resolve_model_list`'s platform-credentials layer.
/// Map a live `think_efforts` block to catalog effort options. The wire
/// token stays the option id/label (`"max"` → label `"Max"`) so the UI
/// mirrors the server's vocabulary, while the canonical value maps through
@@ -612,6 +603,12 @@ fn think_efforts_to_options(
.collect()
}
/// Map one F4 wire model to a catalog entry config.
///
/// SECURITY: the entry carries only env-var NAMES (`env_key`) for the open
/// platforms — never key values — because raw fetched entries are persisted
/// to the models disk cache. Config-file keys are stamped in-memory later by
/// `resolve_model_list`'s platform-credentials layer.
pub(crate) fn platform_wire_model_to_entry(
platform: kigi_models::PlatformId,
wire: kigi_models::WireModel,
@@ -1,13 +1,9 @@
#![cfg_attr(rustfmt, rustfmt::skip)]
#![allow(unused_imports)]
//! [`acp::Agent`] trait implementation for [`MvpAgent`].
//! Co-located child of `mvp_agent` (`use super::*`).
use super::*;
#[async_trait::async_trait(?Send)]
impl acp::Agent for MvpAgent {
/// In the meta, we provide
/// - model_state: the model state, useful for the client to display available models and the default model.
///
/// SINGLE-CALL INVARIANT: this method is the sole writer of
/// `self.auth_method_id` during initialization. It is called exactly once
/// per agent process by the ACP server before any session-creating
@@ -527,7 +523,7 @@ impl acp::Agent for MvpAgent {
})?;
// C1: hot-swap FIRST, then let the authority read the fresh
// token back where it belongs. Nothing hand-carries `auth.key`
// to the shared config any more — the stamp is whatever
// to the shared config — the stamp is whatever
// credential governs that config's own model + endpoint, which
// for a session whose current model is another provider's
// subscription model is that provider's pooled token, and for a
@@ -890,8 +886,7 @@ impl acp::Agent for MvpAgent {
Some(serde_json::json!({ "cwd" : cwd.as_str() })),
);
let models = if is_chat_kind {
// The kimi.com chat-mode model picker was removed with the xAI
// proxy; a chat-kind session has no managed catalog to offer.
// A chat-kind session has no managed catalog to offer.
chat_new_session_model_state(
acp::SessionModelState::new(acp::ModelId::from(String::new()), Vec::new()),
session_initial_model
@@ -72,13 +72,12 @@ impl MvpAgent {
/// ([`crate::session::acp_session::sampler_turn::aux_bearer_resolver_for`]),
/// not a second copy of it.
///
/// M3 completed: the aux path was gated on the session-token gate and this
/// one was not, so an api-key / house-key session whose
/// `[model.session-summary]` block carries its OWN `env_key` on the
/// session's own coding endpoint had that key REPLACED on the wire by the
/// Without the shared session-token gate, an api-key / house-key session
/// whose `[model.session-summary]` block carries its OWN `env_key` on the
/// session's own coding endpoint has that key REPLACED on the wire by the
/// primary bearer on every summary request. Named (rather than inlined
/// above) so the gate is reachable from a test — the ungated version stayed
/// green because the resolver is consumed by `OaiCompatClient::new`.
/// above) so the gate is reachable from a test — the resolver is otherwise
/// consumed by `OaiCompatClient::new`.
///
/// Aux slugs are not the session's selection, so `current_key = None`
/// (H-b then refuses a collided slug rather than guessing its OAuth twin).
@@ -97,7 +96,6 @@ impl MvpAgent {
base_url,
)
}
/// `true` for session-based ACP auth methods.
fn is_session_based_auth(&self) -> bool {
self.auth_method_id
.load()
@@ -109,7 +107,6 @@ impl MvpAgent {
pub(super) fn set_auth_method(&self, id: acp::AuthMethodId) {
self.auth_method_id.store(Some(std::sync::Arc::new(id)));
}
/// Return auth for sync config construction.
pub(super) fn current_or_buffered_auth(&self) -> Option<crate::auth::KimiAuth> {
self.auth_manager
.current()
@@ -134,11 +131,10 @@ impl MvpAgent {
///
/// Memoizes the single [`folder_trust::resolve_launch_dir_trust`] gather (see
/// it for the dedup + TOCTOU contract) so the two one-shot init helpers
/// (`ensure_plugin_registry` and `ensure_local_workspace_ops`) share it
/// instead of each re-scanning. They share a single point-in-time verdict
/// rather than two independent re-scans; the sub-millisecond, startup-only
/// window between them is intentional (the cross-session TOCTOU re-scan is
/// preserved per the contract).
/// (`ensure_plugin_registry` and `ensure_local_workspace_ops`) share one
/// point-in-time verdict instead of each re-scanning. The sub-millisecond,
/// startup-only window between them is intentional (the cross-session TOCTOU
/// re-scan is preserved per the contract).
fn prime_launch_dir_trust(&self) -> (&std::path::Path, bool) {
let trust = *self
.launch_dir_trust
@@ -195,7 +191,7 @@ impl MvpAgent {
}
/// Build the launch-dir plugin registry snapshot on first use.
///
/// Boot-time discovery was deferred past ACP `initialize` (the cwd→git-root
/// Boot-time discovery is deferred past ACP `initialize` (the cwd→git-root
/// plus user/marketplace walks stalled kigi-desktop's first `initialize`),
/// leaving `plugin_registry_handle` empty. That shared snapshot still backs
/// the launch-dir plugin MCP/LSP merges read in `resolve_mcp_servers` and
@@ -232,7 +228,6 @@ impl MvpAgent {
&self.cfg.borrow().compat_resolved,
)
}
/// Set the memory configuration (called from TUI after config resolution).
pub fn set_memory_config(&mut self, config: crate::config::MemoryConfig) {
self.memory_config = if config.enabled { Some(config) } else { None };
}
@@ -583,7 +578,7 @@ impl MvpAgent {
reauth = auth_meta.reauth,
"auth: generic oauth device login",
);
// M4: the SAME home the pool reads from
// The SAME home the pool reads from
// (`oauth_registry::pool_home()`), not `kigi_home()` directly —
// identical in production, but a login driven from a lib test would
// otherwise write into the developer's real `~/.kigi` while every
@@ -637,8 +632,7 @@ impl MvpAgent {
/// Re-resolve eagerly-resolved config fields from the local config.
///
/// Called on `/new` session creation so feature flags reflect the latest
/// on-disk config without requiring a TUI restart. (Formerly this also
/// re-fetched the xAI proxy's remote settings; that endpoint is gone.)
/// on-disk config without requiring a TUI restart.
///
/// In-flight sessions are unaffected — they snapshot config at creation.
pub(super) async fn refresh_settings_and_reapply(&self) {
@@ -819,7 +813,6 @@ impl MvpAgent {
c
}
}
/// Resolve `AgentDefinition.model` override for the parent session.
/// Apply a profile's pinned-model override to the session's sampling config.
///
/// `pinned_model` is resolved once by the caller (shared with harness
@@ -841,7 +834,6 @@ impl MvpAgent {
);
(id.clone(), new_config)
}
/// Build deploy-service config. The tool talks directly to the deployer service.
pub(super) fn prepare_app_builder_deployer_config(
&self,
) -> kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig {
@@ -1062,7 +1054,7 @@ impl MvpAgent {
/// client disconnected and these sessions lost their IPC owner.
///
/// **This is the no-evict keystone.** A disconnect must
/// NOT destroy a session. The behavior is now *detach + keep-resident +
/// NOT destroy a session. The behavior is *detach + keep-resident +
/// idle-unload*:
///
/// - **Sessions with live work stay resident.** We do NOT send `Shutdown`
@@ -1254,15 +1246,12 @@ impl MvpAgent {
let _ = tokio::time::timeout(deadline - now, rx.changed()).await;
}
}
/// Returns the default YOLO mode setting for new sessions
pub fn default_yolo_mode(&self) -> bool {
self.default_yolo_mode
}
/// Returns the storage mode configured for this agent
pub fn storage_mode(&self) -> StorageMode {
self.storage_mode
}
/// Returns the background copy context for managing background file copy tasks.
pub fn background_copy_context(&self) -> BackgroundCopyContext {
self.background_copy_context.clone()
}
@@ -1326,7 +1315,6 @@ impl MvpAgent {
) -> Vec<crate::agent::subagent::RunningSubagentListSeed> {
self.subagent_coordinator.borrow().list_running_for_parent(parent_session_id)
}
/// Return fork provenance metadata for a subagent.
pub(crate) fn provenance_for_subagent(
&self,
subagent_id: &str,
@@ -1393,14 +1381,10 @@ impl MvpAgent {
Err(_) => Err("timeout"),
}
}
/// Get a session's cwd by session_id.
/// Returns None if the session is not found.
pub fn get_session_cwd(&self, session_id: &acp::SessionId) -> Option<PathBuf> {
let sessions = self.sessions.borrow();
sessions.get(session_id).map(|handle| PathBuf::from(&handle.info.cwd))
}
/// Get a session handle by session_id.
/// Returns None if the session is not found.
pub fn get_session_handle(
&self,
session_id: &acp::SessionId,
@@ -1626,25 +1610,25 @@ impl MvpAgent {
/// returns it verbatim whenever a model id fails to resolve, and
/// `SubagentSpawnContext` clones it as every subagent's baseline — so an
/// `api_key` stamped here reaches the wire against whatever `base_url` the
/// config carries. The login/seed sites used to stamp it unconditionally,
/// exactly the mistake `authenticate_oauth_platform` already documents ("Do
/// NOT stamp this token onto the shared sampling_config").
/// config carries. Stamping it unconditionally is exactly the mistake
/// `authenticate_oauth_platform` documents ("Do NOT stamp this token onto
/// the shared sampling_config").
///
/// C1: this function does NOT take a credential. The previous shape took
/// `key: String` — always the primary Kimi bearer — and guarded it with a
/// predicate asking whether **a** session credential may ride. For a
/// subscription-OAuth platform at its own host that is correctly `true`, but
/// the credential that may ride there is that platform's POOLED token: a
/// Claude Pro/Max user running `kigi login` stamped the Kimi subscription
/// bearer onto a config routed at `api.anthropic.com`. Asking
/// `credential_for` instead makes the question and the credential the same
/// object, so the pairing cannot be wrong — and there is no primary handle
/// here to hand-carry (M2).
/// C1: this function does NOT take a credential — it asks `credential_for`,
/// so the question ("may a session credential ride here?") and the
/// credential itself are the same object and cannot be mispaired. Taking a
/// `key: String` (always the primary Kimi bearer) guarded by a separate
/// predicate mispairs them: for a subscription-OAuth platform at its own
/// host the predicate is correctly `true`, but the credential that may ride
/// there is that platform's POOLED token, not the primary — so a Claude
/// Pro/Max user running `kigi login` stamped the Kimi subscription bearer
/// onto a config routed at `api.anthropic.com`. There is no primary handle
/// here to hand-carry either.
///
/// `overwrite = false` keeps the historical "only if missing" seeding
/// behaviour; the login handlers pass `true` because a fresh login must
/// replace a stale bearer, and they call this AFTER the manager holds the
/// new token so it is read back through the authority.
/// `overwrite = false` seeds only when `api_key` is missing; the login
/// handlers pass `true` because a fresh login must replace a stale bearer,
/// and they call this AFTER the manager holds the new token so it is read
/// back through the authority.
///
/// SECURITY: the token is never logged.
pub(super) fn stamp_session_credential(&self, overwrite: bool) -> bool {
@@ -2448,7 +2432,7 @@ impl MvpAgent {
// Classify the credential's auth_type against the bearer this
// model's endpoint would ACTUALLY receive, not the raw primary:
// an API-key-platform model has no session credential at all, and
// reading the primary here reported one.
// reading the primary here would report one.
//
// H-a: the platform is resolved with the SAME catalog key the
// actor about to be spawned seeds itself with
@@ -1,5 +1,4 @@
//! Code-navigation eligibility gating and codebase-index management for [`MvpAgent`].
//! Co-located child of `mvp_agent` (`use super::*`).
use super::*;
@@ -41,10 +40,11 @@ impl MvpAgent {
Some((handle, was_newly_started))
}
/// Core eligibility check — pure function that accepts explicit client
/// context rather than reading global agent state.
/// Core eligibility check — takes client type and capability as explicit
/// arguments instead of the last-initialize global fields, so it is correct
/// under leader mode.
///
/// This is the single place that applies all four gates. Call it via
/// The single place that applies all four gates. Call it via
/// [`code_nav_eligibility_for_request`] (leader-mode safe) or
/// [`code_nav_eligibility`] (global state, non-leader use only).
pub(super) fn code_nav_eligibility_inner(
@@ -55,7 +55,6 @@ impl MvpAgent {
) -> Result<(), CodeNavEligibility> {
use crate::agent::config::CodebaseIndexingSetting;
// Gate 1: client type
if !matches!(client_type, ClientType::KigiWeb) {
tracing::info!(
client_type = ?client_type,
@@ -66,7 +65,6 @@ impl MvpAgent {
return Err(CodeNavEligibility::ClientNotWeb);
}
// Gate 2: capability advertised
if !code_nav_enabled {
tracing::info!(
gate = "capability",
@@ -76,7 +74,6 @@ impl MvpAgent {
return Err(CodeNavEligibility::CapabilityNotAdvertised);
}
// Gate 3: config
let setting = self.cfg.borrow().features.codebase_indexing.clone();
if let CodebaseIndexingSetting::Enabled(false) = &setting {
tracing::info!(
@@ -87,7 +84,6 @@ impl MvpAgent {
return Err(CodeNavEligibility::DisabledByConfig);
}
// Gate 4: git root / config globs
let git_root = kigi_workspace::session::git::find_git_root_from_path(cwd).ok();
match &setting {
CodebaseIndexingSetting::Enabled(true) => {
@@ -113,7 +109,8 @@ impl MvpAgent {
return Err(CodeNavEligibility::DisabledByConfig);
}
}
CodebaseIndexingSetting::Enabled(false) => {} // handled above
// Enabled(false) already returned above.
CodebaseIndexingSetting::Enabled(false) => {}
}
Ok(())
@@ -128,7 +125,8 @@ impl MvpAgent {
/// called once per connection; the global fields on `MvpAgent` reflect
/// only the **last** client to call `initialize()`.
///
/// Falls back to global agent state when no session_id is given.
/// Rejects with `SessionRequired` when no `session_id` is given or the
/// session is unknown, rather than falling back to global state.
pub fn code_nav_eligibility_for_request(
&self,
session_id: Option<&acp::SessionId>,
@@ -214,7 +212,6 @@ impl MvpAgent {
}
let target = git_root.unwrap_or_else(|| cwd.to_path_buf());
// get_or_create returns the authoritative (handle, was_newly_started) pair.
// Log only on actual first spawn so reuse requests are not misleadingly
// labelled as "starting".
let (handle, was_newly_started) = self.get_or_create_codebase_index(target.clone());
@@ -250,8 +247,6 @@ impl MvpAgent {
self.codebase_indexes.lock().get_or_create(cwd)
}
/// Get an existing codebase index for the given cwd.
/// Returns None if no index exists for this cwd.
pub fn get_codebase_index(
&self,
cwd: &std::path::Path,
@@ -9,7 +9,7 @@
//! client advertises the capability until the desktop UI ships — so this is
//! inert by default even with the feature flag on. The TUI/headless clients never
//! advertise it (they self-gate trust client-side), so they are never
//! double-prompted. Co-located child of `mvp_agent` (`use super::*`).
//! double-prompted.
//!
//! Post-grant reload scope: MCP, plugins, and each session's own project hooks
//! are hot-reloaded in place — for EVERY session sharing the granted workspace
@@ -22,8 +22,6 @@ pub(crate) struct LocalRef<T> {
ptr: *const T,
}
impl<T> LocalRef<T> {
/// Create a `LocalRef` from a shared reference.
///
/// # Safety contract (enforced by the caller, not by the type system)
///
/// The referenced `T` must live for the entire duration of the `LocalSet`
@@ -31,8 +29,6 @@ impl<T> LocalRef<T> {
pub(crate) fn new(val: &T) -> Self {
Self { ptr: val as *const T }
}
/// Dereference back to `&T`.
///
/// # Safety
///
/// Safe because the caller of `new()` guarantees the pointee is alive
@@ -29,7 +29,6 @@ fn includes_baseline_keys_without_usage() {
assert_eq!(meta["promptId"], "prompt-1");
assert_eq!(meta["totalTokens"], 42_000);
assert_eq!(meta["modelId"], "kigi-4.5");
// No per-turn keys when usage is absent.
assert!(meta.get("inputTokens").is_none());
assert!(meta.get("outputTokens").is_none());
assert!(meta.get("cachedReadTokens").is_none());
@@ -107,21 +106,18 @@ fn usage_object_lands_on_meta() {
#[test]
fn cancel_trigger_lands_as_camelcase_meta_key() {
// A send-now cancelled turn's PromptResponse `_meta` carries `cancelTrigger: "send_now"`.
let meta = build_prompt_response_meta(PromptResponseMetaArgs {
cancel_trigger: Some("send_now".to_string()),
..args("s", "p", 0, "m")
});
assert_eq!(meta["cancelTrigger"], "send_now");
// Absent for non-cancel completions — the key must not appear.
let none = build_prompt_response_meta(args("s", "p", 0, "m"));
assert!(none.get("cancelTrigger").is_none());
}
#[test]
fn structured_output_maps_to_camelcase_meta_keys() {
// Success carries the validated value under `structuredOutput`; no error key.
let ok = build_prompt_response_meta(PromptResponseMetaArgs {
structured_output: Some(Ok(serde_json::json!({"name": "ada"}))),
..args("s", "p", 0, "m")
@@ -129,7 +125,6 @@ fn structured_output_maps_to_camelcase_meta_keys() {
assert_eq!(ok["structuredOutput"]["name"], "ada");
assert!(ok.get("structuredOutputError").is_none());
// Failure carries the message under `structuredOutputError`; no value key.
let err = build_prompt_response_meta(PromptResponseMetaArgs {
structured_output: Some(Err("output does not match the required schema".to_string())),
..args("s", "p", 0, "m")
@@ -140,7 +135,6 @@ fn structured_output_maps_to_camelcase_meta_keys() {
);
assert!(err.get("structuredOutput").is_none());
// No schema requested → neither key present.
let none = build_prompt_response_meta(args("s", "p", 0, "m"));
assert!(none.get("structuredOutput").is_none());
assert!(none.get("structuredOutputError").is_none());
@@ -3,7 +3,6 @@
//! block keeps access to `MvpAgent`'s private fields.
use super::*;
impl MvpAgent {
/// Ask a live session actor to shut down.
pub(crate) fn request_session_shutdown(&self, id: &acp::SessionId) {
if let Some(handle) = self.sessions.borrow().get(id) {
let _ = handle.cmd_tx.send(SessionCommand::Shutdown);
@@ -46,7 +45,7 @@ impl MvpAgent {
self.session_live_state.borrow_mut().remove(id);
}
/// Get-or-create the per-session prompt-intake lock (see
/// [`Self::prompt_intake_locks`]). Cheap clone of the shared `Rc`.
/// [`Self::prompt_intake_locks`]).
pub(super) fn prompt_intake_lock(
&self,
id: &acp::SessionId,
@@ -64,13 +63,11 @@ impl MvpAgent {
self.finalize_session_replica(id);
self.remove_session_terminal(id, SessionLiveState::Completed);
}
/// Record the coarse lifecycle state for a session.
pub(super) fn set_session_live_state(&self, id: &acp::SessionId, state: SessionLiveState) {
self.session_live_state
.borrow_mut()
.insert(id.clone(), state);
}
/// Read the recorded lifecycle state for a session (test observability).
#[cfg(test)]
pub(super) fn session_live_state_for(&self, id: &acp::SessionId) -> Option<SessionLiveState> {
self.session_live_state.borrow().get(id).copied()
@@ -188,8 +185,6 @@ impl MvpAgent {
_ => RosterActivity::Idle,
}
}
/// Build a single roster entry for a resident session, or `None` if it is
/// not currently resident.
pub(super) fn resident_roster_entry(
&self,
id: &acp::SessionId,
@@ -224,7 +219,6 @@ impl MvpAgent {
origin: crate::agent::roster::RosterOrigin::Local,
})
}
/// Snapshot all resident sessions as roster entries (synchronous; no disk).
pub(super) fn resident_roster_entries(&self) -> Vec<crate::agent::roster::RosterEntry> {
let ids: Vec<acp::SessionId> = self.sessions.borrow().keys().cloned().collect();
ids.iter()
@@ -243,7 +237,6 @@ impl MvpAgent {
self.cache_resident_titles(&entries);
entries
}
/// Refresh `resident_roster_titles` from the freshly-built roster.
pub(super) fn cache_resident_titles(&self, entries: &[crate::agent::roster::RosterEntry]) {
*self.resident_roster_titles.borrow_mut() = entries
.iter()
@@ -265,8 +258,7 @@ impl MvpAgent {
/// Reap a session whose **resident** actor thread exited unexpectedly
/// (panic / load failure). Demotes it to `DeadFailed`, emits the roster
/// delta, and removes it WITHOUT finalize — the conversation persists on
/// disk and stays resumable (reaping a dead actor is harmless;
/// it demotes to Dormant).
/// disk and stays resumable.
pub(super) fn reap_dead_session(&self, id: &acp::SessionId) {
self.remove_session_terminal(id, SessionLiveState::DeadFailed);
}
@@ -2,14 +2,9 @@
//! Co-located child of `mvp_agent` (`use super::*`); tested by `tests/subagent_spawn_context_tests.rs`.
use super::*;
impl MvpAgent {
/// Start the subagent coordinator drain task.
///
/// Takes the `subagent_event_rx` receiver (once) and spawns a `spawn_local` task
/// that receives `SubagentRequest`s and delegates each to
/// `handle_subagent_request()` on its own `spawn_local` task.
///
/// Uses `LocalRef` to reference `self` from
/// `spawn_local` closures. Idempotent: subsequent calls are no-ops.
/// Start the subagent coordinator drain task: receive `SubagentEvent`s and
/// dispatch each. Idempotent — the `subagent_event_rx` receiver is taken
/// once, so subsequent calls are no-ops.
pub(super) fn start_subagent_coordinator(&self) {
let Some(mut rx) = self.subagent_event_rx.borrow_mut().take() else {
return;
@@ -264,16 +259,12 @@ impl MvpAgent {
cli_agent_names,
}
}
/// Build a `SubagentSpawnContext` from the current agent state and the
/// parent session's shared resources.
///
/// This is the ONLY subagent-related method on MvpAgent besides the
/// coordinator startup.
/// Build a spawn context for a real subagent spawn. The parent session is
/// guaranteed present here because the parent just issued the spawn request,
/// so a missing parent is a real invariant violation and panics. Read-only
/// callers that can race a parent teardown (e.g. `DescribeType`) must use
/// [`Self::try_build_subagent_spawn_context`] instead.
/// Build a `SubagentSpawnContext` for a real subagent spawn. The parent
/// session is guaranteed present here because the parent just issued the
/// spawn request, so a missing parent is a real invariant violation and
/// panics. Read-only callers that can race a parent teardown (e.g.
/// `DescribeType`) must use [`Self::try_build_subagent_spawn_context`]
/// instead.
pub(super) fn build_subagent_spawn_context(
&self,
parent_session_id: &str,
@@ -186,7 +186,6 @@ async fn broadcast_refresh_skill_baseline_tolerates_dropped_receiver() {
}
/// The monotonic turn counter must never wrap on the DB-bound i32 path.
/// `allocate_turn_number` returns u64; the AB submission casts to i32.
/// Verify we saturate instead of wrapping.
#[test]
fn trace_turn_to_i32_saturates_at_max() {
let small: u64 = 42;
@@ -221,7 +220,6 @@ fn allocate_turn_number_advances_counter() {
assert_eq!(allocate(&sid), 2);
assert_eq!(*counters.borrow().get(&sid).unwrap(), 3);
}
/// With no overrides and model_agent_type = None, the default agent is used.
#[test]
#[serial_test::serial]
fn resolve_agent_definition_defaults_to_kigi() {
@@ -242,8 +240,6 @@ fn resolve_agent_definition_defaults_to_kigi() {
unsafe { std::env::set_var("KIGI_AGENT", v) }
}
}
/// When model_agent_type = Some("codex"), the codex agent is selected even
/// though the default chain would return kigi.
#[test]
#[serial_test::serial]
fn resolve_agent_definition_model_agent_type_overrides_default() {
@@ -319,8 +315,7 @@ fn resolve_agent_definition_acp_profile_wins_when_model_agent_type_is_default()
unsafe { std::env::set_var("KIGI_AGENT", v) }
}
}
/// Regression: after `DEFAULT_AGENT_TYPE` flipped to
/// `kigi-plan`, models in the catalog that still declare
/// Regression: models in the catalog that still declare
/// `agent_type = "kigi"` explicitly must NOT preempt an ACP
/// profile. Any value in the `kigi*` family is the stock harness
/// with no strict requirement.
@@ -406,7 +401,6 @@ fn resolve_agent_definition_cli_agent_profile_wins_when_model_agent_type_is_defa
unsafe { std::env::set_var("KIGI_AGENT", v) }
}
}
/// Agent profile with `model: Override(id)` preserves the field through resolution.
#[test]
#[serial_test::serial]
fn resolve_agent_definition_agent_profile_with_model_override() {
@@ -679,7 +673,6 @@ fn file_toolset_override_invalid_config_returns_error() {
assert!(err.is_err());
assert!(err.unwrap_err().contains("unknown"));
}
/// Helper: creates a real SessionHandle with the given model, yolo, and client id.
/// Requires a tokio runtime for SessionSignalsHandle::new().
fn make_test_handle(
model: &str,
@@ -751,7 +744,6 @@ fn make_test_handle(
scheduler_handle: None,
}
}
/// lookup_session_model returns the per-session model for each session.
#[tokio::test]
async fn lookup_session_model_returns_per_session_model() {
let sid_a = acp::SessionId::new("sess-a");
@@ -775,7 +767,6 @@ async fn lookup_session_model_returns_per_session_model() {
"codex-mini"
);
}
/// lookup_session_model falls back to the default when session_id is None.
#[tokio::test]
async fn lookup_session_model_fallback_no_session() {
let default_model = acp::ModelId::new("kigi-3");
@@ -787,7 +778,6 @@ async fn lookup_session_model_fallback_no_session() {
"kigi-3"
);
}
/// Mutating session A's model_id via the handle does not affect session B.
#[tokio::test]
async fn set_session_model_does_not_cross_contaminate() {
let sid_a = acp::SessionId::new("sess-a");
@@ -895,7 +885,6 @@ async fn session_config_options_resolves_routing_slug_to_catalog_model() {
"resolved catalog model must be selected"
);
}
/// YOLO toggle scoped by client_identifier: only matching sessions are updated.
#[tokio::test]
async fn yolo_toggle_scoped_by_client_identifier() {
let sid_tui = acp::SessionId::new("sess-tui");
@@ -922,8 +911,6 @@ async fn yolo_toggle_scoped_by_client_identifier() {
"VS Code session must NOT be affected by TUI's yolo toggle"
);
}
/// A client can explicitly disable YOLO for its own sessions after startup,
/// even if those sessions were initially created with yolo=true.
#[tokio::test]
async fn yolo_toggle_can_disable_session_started_with_yolo_enabled() {
let sid_tui = acp::SessionId::new("sess-tui");
@@ -950,8 +937,6 @@ async fn yolo_toggle_can_disable_session_started_with_yolo_enabled() {
"other client's session must keep its previous yolo state"
);
}
/// `drain_old_session_thread` returns immediately when the thread has
/// already finished.
#[tokio::test]
async fn drain_finished_thread_returns_immediately() {
let session_threads: RefCell<HashMap<acp::SessionId, crate::session::SessionThread>> =
@@ -967,7 +952,6 @@ async fn drain_finished_thread_returns_immediately() {
assert!(thread.is_finished(), "thread should be finished");
assert!(!session_threads.borrow().contains_key(&sid));
}
/// `drain_old_session_thread` waits for a slow thread to finish.
#[tokio::test]
async fn drain_waits_for_slow_thread() {
let session_threads: RefCell<HashMap<acp::SessionId, crate::session::SessionThread>> =
@@ -994,7 +978,6 @@ async fn drain_waits_for_slow_thread() {
}
assert!(thread.is_finished());
}
/// Drain respects the 5s deadline and returns even if the thread is still running.
#[tokio::test]
async fn drain_respects_deadline() {
let session_threads: RefCell<HashMap<acp::SessionId, crate::session::SessionThread>> =
@@ -1133,7 +1116,6 @@ fn test_sessionless_request_requires_session_id() {
"cwd-only requests with no sessionId must return SessionRequired"
);
}
/// Build a minimal MvpAgent suitable for testing extension methods.
fn build_minimal_agent_for_tests() -> MvpAgent {
use crate::agent::config::Config as AgentConfig;
use crate::auth::{AuthManager, KimiCodeConfig};
@@ -1145,7 +1127,6 @@ fn build_minimal_agent_for_tests() -> MvpAgent {
let cfg = AgentConfig::default();
MvpAgent::new(gateway, &cfg, auth_manager, None).expect("valid test config")
}
/// Build a minimal MvpAgent with pre-loaded auth for gate tests.
fn build_agent_with_auth(auth: crate::auth::KimiAuth) -> MvpAgent {
use crate::agent::config::Config as AgentConfig;
use crate::auth::{AuthManager, KimiCodeConfig};
@@ -1201,12 +1182,6 @@ async fn prepare_sampling_config_never_stamps_kimi_key_on_grok_model() {
// api_key — the byte-identical primary path, and proof the Kimi token is
// live (so it WOULD leak if mis-routed onto a grok request). This assertion
// also confirms the session-based primary path is active.
//
// This used to use `moonshot-cn/kimi-k2-0905-preview` and assert the SAME
// thing, which encoded the C1 defect: moonshot-cn is an API-key registry
// platform on `api.moonshot.cn`, NOT first-party, so "must carry the primary
// session key" was asserting the leak. `api_key_channel_leak_tests` now pins
// the opposite for every moonshot entry.
let mut kimi_model = ModelEntry::fallback("kimi-for-coding", &endpoints);
kimi_model.info.id = Some("kimi-code/kimi-for-coding".to_string());
kimi_model.info.base_url = kigi_env::PRODUCTION_ENDPOINTS.coding_api_base_url.to_string();
@@ -1478,7 +1453,6 @@ async fn push_roster_activity_delta_broadcasts_overridden_activity() {
let changed = drain_roster_changed(&mut rx).expect("turn-end delta emitted");
assert_eq!(changed.upserted[0].activity, RosterActivity::Idle);
}
/// Extract the inner payload from an ExtResponse.
#[expect(
dead_code,
reason = "unused in production; remove expect when wired or delete the item"
@@ -1528,7 +1502,6 @@ async fn test_web_session_with_capability_is_eligible() {
"web session with code-nav capability must be eligible"
);
}
/// TUI session is rejected at gate 1 (client type) regardless of capability.
#[tokio::test]
async fn test_tui_session_is_rejected() {
let sid = acp::SessionId::new("sess-tui");
@@ -1541,7 +1514,6 @@ async fn test_tui_session_is_rejected() {
"TUI client must be rejected at gate 1 (client type)"
);
}
/// Web session without capability is rejected at gate 2.
#[tokio::test]
async fn test_web_session_without_capability_is_rejected() {
let sid = acp::SessionId::new("sess-web-no-cap");
@@ -1554,8 +1526,6 @@ async fn test_web_session_without_capability_is_rejected() {
"web client without capability must be rejected at gate 2"
);
}
/// Leader-mode isolation: two sessions with different code-nav state return
/// independent results.
#[tokio::test]
async fn test_leader_mode_two_sessions_stay_isolated() {
let web_sid = acp::SessionId::new("web");
@@ -1948,7 +1918,7 @@ async fn auth_type_no_method_id_no_current_returns_api_key() {
/// in-memory bearer takes precedence: this is the order observed during
/// `initialize()` silent refresh -- a token is hot-swapped in before
/// `authenticate()` writes the method id. Reporting `SessionToken`
/// here matches pre-fix behavior and keeps logging stable.
/// here keeps logging stable.
#[tokio::test(flavor = "current_thread")]
async fn auth_type_no_method_id_with_current_returns_session_token() {
use crate::auth::KimiAuth;
@@ -2593,9 +2563,8 @@ fn session_live_state_map_is_bounded_across_cycles() {
}
/// Finalize fires on a genuine terminal close — driven through the **real**
/// `kigi/session/close` dispatch (`ext_method` → `handle_session_close`),
/// not the internal helper. Proves finalize was *moved* (not removed) and
/// guards the handler's `existed` gate. (Finalize assertion is
/// invocation-level; see note in `finalize_session_replica`.)
/// not the internal helper. Guards the handler's `existed` gate. (Finalize
/// assertion is invocation-level; see note in `finalize_session_replica`.)
#[test]
fn explicit_close_finalizes_the_replica() {
run_local_for_bridge_test(|| async {
@@ -2697,8 +2666,6 @@ fn supervisor_reaps_panicked_resident_actor() {
);
});
}
/// `ensure_session_supervisor` is idempotent: calling it repeatedly spawns
/// the sweeper loop exactly once.
#[test]
fn ensure_session_supervisor_is_idempotent() {
run_local_for_bridge_test(|| async {
@@ -150,7 +150,7 @@ async fn api_key_platform_models_never_carry_the_kimi_bearer_as_api_key() {
}
/// C2, the `[model.*]` repro. A `[model.gpt-4o]` block has `info.id == None`, so
/// it has no platform at all — which used to be a blanket allow. BYOK is
/// it has no platform at all — the blanket-allow gap the guard closes. BYOK is
/// `has_own_credentials()`, which probes `std::env::var` AT CALL TIME, so an
/// unset (or mistyped) `env_key` classifies the model NotByok and the Kimi
/// bearer went to `api.openai.com` on BOTH channels.
@@ -165,7 +165,8 @@ async fn config_model_with_an_unset_env_key_never_carries_the_kimi_bearer() {
let (_dir, agent) = kimi_session_agent();
let mut entry = ModelEntry::fallback("gpt-4o", &EndpointsConfig::default());
entry.info.id = None; // a `[model.gpt-4o]` config block
// a `[model.gpt-4o]` config block
entry.info.id = None;
entry.info.base_url = "https://api.openai.com/v1".to_string();
entry.env_key = Some(EnvKeys::single("OPENAI_API_KEY_TYPO"));
assert!(
@@ -43,9 +43,7 @@ fn rebuild_shared_config(agent: &MvpAgent) {
/// `base_url`. Zero-config repro: a Kimi subscription + a bundled
/// `moonshot-cn/*` default.
///
/// Revert-to-red (L: this edit COMPILES — the previous wording named a
/// `Option<String>` argument that the `Option<&SessionCredential>` signature
/// rejects, so it could never have been run): in
/// Revert-to-red: in
/// `ModelsManager::sampling_config`, ask the authority about the SESSION's
/// endpoint instead of the current model's own —
/// `.credential_for(None, &config.endpoints.proxy_url())` in place of
@@ -331,9 +329,6 @@ async fn oauth_platform_shared_config_never_receives_the_primary_on_login() {
/// unresolved-model fallback and every subagent baseline turn 401'd until
/// restart.
///
/// The switch happens AFTER the config is built — the previous version of this
/// test left both on the same model, so it could not catch this.
///
/// Revert-to-red (production, compiles): make `MvpAgent::shared_config_platform`
/// re-resolve from the live cell instead of returning the captured value —
/// ```ignore
@@ -35,7 +35,8 @@ async fn subagent_spawn_context_inherits_parent_permission_handle() {
pattern: Some("**/.env".to_owned()),
pattern_mode: PatternMode::Glob,
}])),
Vec::new(), // deny_read_globs
// deny_read_globs
Vec::new(),
Vec::new(),
false,
None,
@@ -3,11 +3,8 @@
//! into the JSON shape emitted by `LoadSession` on `_meta.codeRestore`.
use kigi_workspace::session::git::{CheckoutSessionOutcome, RestoreKind, build_restore_decision};
use serde_json::Value;
/// Build the `codeRestore` JSON meta, or `None` when no restore should
/// be reported (no checkout AND no archive applied). The shared
/// [`build_restore_decision`] is the source of truth; this function
/// only adapts the result into the wire JSON shape used by the
/// non-worktree path.
/// Build the `codeRestore` JSON meta for the non-worktree path, or `None`
/// when no restore should be reported (no checkout AND no archive applied).
pub(crate) fn build_code_restore_meta(
target_sha: &str,
outcome: &CheckoutSessionOutcome,
@@ -18,11 +18,6 @@ use serde::{Deserialize, Serialize};
use crate::session::persistence::Summary;
/// Coarse activity of a session as rendered in the dashboard's status column.
///
/// Mirrors the design's `SessionActivity` at dashboard granularity. A full
/// background-work breakdown (bg tasks / monitors / scheduler / subagents)
/// lands with a richer `SessionActivity`; the dashboard only needs this
/// coarse signal to pick a status glyph.
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RosterActivity {
+1 -12
View File
@@ -48,9 +48,7 @@ use indexmap::IndexMap;
/// Swappable destination for the relay task.
///
/// Points at the current ACP connection's gateway sender. When no client is
/// connected, the value is `None` and outbound messages are silently dropped
/// (matching the old behaviour where the gateway channel's receiver was simply
/// gone).
/// connected, the value is `None` and outbound messages are silently dropped.
type RelayDest = Rc<RefCell<Option<mpsc::UnboundedSender<AcpClientMessage>>>>;
const MAX_BUFFER_SIZE: usize = 8 * 1024 * 1024;
@@ -59,7 +57,6 @@ const KEEPALIVE_INTERVAL_SECS: u64 = 15;
/// Configuration for the agent WebSocket server.
#[derive(Debug, Clone)]
pub struct ServerConfig {
/// Address to bind the server to
pub bind_addr: SocketAddr,
/// Secret token for client authentication (required)
pub secret: String,
@@ -90,7 +87,6 @@ pub struct WsQueryParams {
/// Validate the bearer token from request headers or query parameters.
fn validate_auth(headers: &HeaderMap, query: &WsQueryParams, expected_secret: &str) -> bool {
// Try Authorization header
if let Some(token) = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
@@ -115,7 +111,6 @@ async fn ws_handler(
headers: HeaderMap,
Query(query): Query<WsQueryParams>,
) -> Response {
// Validate secret token from header or query param
if !validate_auth(&headers, &query, &state.secret) {
warn!("Unauthorized connection attempt from {}", addr);
return (
@@ -139,7 +134,6 @@ async fn handle_connection(ws: WebSocket, state: Arc<ServerState>, peer_addr: So
let (mut ws_write, mut ws_read) = ws.split();
// Channels for bridging WS <-> Agent thread
let (to_agent_tx, to_agent_rx) = mpsc::unbounded_channel::<String>();
let (from_agent_tx, mut from_agent_rx) = mpsc::unbounded_channel::<String>();
@@ -149,7 +143,6 @@ async fn handle_connection(ws: WebSocket, state: Arc<ServerState>, peer_addr: So
{
let mut agent_tx_guard = state.agent_conn_tx.lock().await;
// Check if existing sender is still alive (receiver not dropped)
if let Some(ref tx) = *agent_tx_guard
&& tx.is_closed()
{
@@ -203,7 +196,6 @@ async fn handle_connection(ws: WebSocket, state: Arc<ServerState>, peer_addr: So
info!("Persistent agent thread spawned");
}
// Send new WS channels to the agent thread
if let Some(ref tx) = *agent_tx_guard
&& tx
.send(NewConnectionChannels {
@@ -339,7 +331,6 @@ async fn run_persistent_agent(
}
});
// Accept new connections in a loop
while let Some(channels) = connection_rx.recv().await {
info!("Agent thread: setting up new ACP connection (reconnect)");
setup_acp_connection(agent.clone(), channels, relay_dest.clone());
@@ -361,7 +352,6 @@ fn setup_acp_connection(
to_ws_tx,
} = channels;
// Create new simplex IO streams for this ACP connection
let (agent_read_rx, mut agent_read_tx) = simplex(MAX_BUFFER_SIZE);
let (agent_write_rx, agent_write_tx) = simplex(MAX_BUFFER_SIZE);
@@ -372,7 +362,6 @@ fn setup_acp_connection(
// The relay task will forward persistent-channel messages here.
let (conn_gw_tx, conn_gw_rx) = tokio::sync::mpsc::unbounded_channel::<AcpClientMessage>();
// Point the relay at this new connection's channel
*relay_dest.borrow_mut() = Some(conn_gw_tx);
// Create new ACP connection reusing the same MvpAgent (via Rc clone).
@@ -66,7 +66,6 @@ fn effort_label(effort: ReasoningEffort) -> String {
}
/// The built-in session-picker modes used when the model has no server list.
/// Reproduces the historical five rows and their labels.
pub(crate) fn legacy_session_effort_options() -> Vec<ReasoningEffortOption> {
SELECTABLE_REASONING_EFFORTS
.iter()
@@ -9,10 +9,8 @@ use anyhow::{Context, Result};
use reqwest::RequestBuilder;
use serde::{Deserialize, Serialize};
// ============================================================================
// Request / response types (local — not in cli-chat-proxy since these
// are only used by the agent, not consumed by other crates)
// ============================================================================
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -35,7 +33,7 @@ pub struct RegisterRequest {
pub device_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_session_id: Option<String>,
// --- Subagent-specific fields (optional, backward-compatible) ---
// Subagent-specific fields (optional, backward-compatible).
#[serde(skip_serializing_if = "Option::is_none")]
pub session_kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -67,10 +65,6 @@ pub struct UpdateRequest {
pub restorable_turn_number: Option<i32>,
}
// ============================================================================
// Response types
// ============================================================================
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionRecord {
@@ -132,10 +126,6 @@ pub struct DownloadResponse {
pub turn: i32,
}
// ============================================================================
// Client
// ============================================================================
#[derive(Clone)]
pub struct SessionRegistryClient {
raw_client: reqwest::Client,
@@ -387,7 +377,7 @@ impl SessionRegistryClient {
mod tests {
use super::*;
// ── UpdateRequest wire shapes ────────────────────────────────────────────
// UpdateRequest wire shapes.
//
// The writer split relies on two distinct update payloads being sent at
// different times:
@@ -495,7 +485,7 @@ mod tests {
assert!(json.get("device_id").is_none());
}
// ── SessionRecord backward compatibility ─────────────────────────────────
// SessionRecord backward compatibility.
//
// Older servers do not include `restorable_turn_number` in their response.
// The field is `#[serde(default)]` so it must deserialize as `None` when
@@ -69,7 +69,6 @@ impl SubagentCoordinator {
std::sync::atomic::Ordering::Relaxed,
);
}
/// Returns a handle to the completion [`Notify`].
#[cfg_attr(
not(test),
expect(
@@ -80,11 +79,9 @@ impl SubagentCoordinator {
pub fn completion_notify(&self) -> Arc<Notify> {
Arc::clone(&self.completion_notify)
}
/// Returns a shared handle to the turn-active flag.
pub fn turn_active_flag(&self) -> Arc<std::sync::atomic::AtomicBool> {
Arc::clone(&self.is_turn_active)
}
/// Whether the model's turn is currently active.
#[cfg_attr(
not(test),
expect(
@@ -163,7 +160,6 @@ impl SubagentCoordinator {
subagent_usage_not_applied: self.subagent_usage_not_applied(prompt_id),
}
}
/// Drain all buffered completion summaries, returning them and clearing the buffer.
pub fn drain_pending_completions(&mut self) -> Vec<SubagentCompletionSummary> {
std::mem::take(&mut self.pending_completions)
}
@@ -235,8 +231,6 @@ impl SubagentCoordinator {
cancelled: false,
});
}
/// Insert a synthetic failed entry, push a completion summary, notify waiters.
/// Clears any stale pending entry for the same id.
fn record_failure_completion(&mut self, c: FailureCompletion<'_>) {
self.pending.remove(&c.subagent_id);
self.sync_running_gauge();
@@ -303,8 +297,6 @@ impl SubagentCoordinator {
self.active.insert(tracker.subagent_id.clone(), tracker);
self.sync_running_gauge();
}
/// Move a finished subagent from `active` to `completed`.
/// Returns the tracker if it was active.
pub fn move_to_completed(
&mut self,
id: &str,
@@ -448,7 +440,6 @@ impl SubagentCoordinator {
}
SubagentCancelOutcome::NotFound
}
/// Internal: send Cancel + Shutdown to a tracked subagent.
fn cancel_tracker(tracker: &SubagentTracker) {
tracker.cancel_token.cancel();
let _ = tracker
@@ -91,9 +91,6 @@ impl SubagentCoordinator {
}
None
}
/// Return `(parent_session_id, child_session_id)` for a given subagent.
///
/// Checks active first, then completed. Returns `None` if not found.
pub(crate) fn session_ids_for(&self, id: &str) -> Option<(String, String)> {
if let Some(t) = self.active.get(id) {
return Some((t.parent_session_id.clone(), t.child_session_id.0.to_string()));
@@ -135,7 +132,7 @@ impl SubagentCoordinator {
self.mark_block_waited(id);
self.block_wait_slots.entry(id.to_string()).or_default().push(slot);
}
/// Drop a previously registered reply slot (query poll loop exited).
/// Drop a registered reply slot (query poll loop exited).
pub(crate) fn unregister_block_wait(&mut self, id: &str, slot: &BlockWaitSlot) {
if let Some(slots) = self.block_wait_slots.get_mut(id) {
slots.retain(|s| !std::rc::Rc::ptr_eq(s, slot));
@@ -181,7 +178,6 @@ impl SubagentCoordinator {
self.active.get(id).is_some_and(|t| t.explicitly_killed)
|| self.completed.get(id).is_some_and(|c| c.explicitly_killed)
}
/// Return fork provenance for a given subagent.
pub(crate) fn provenance_for(&self, id: &str) -> SubagentProvenance {
if let Some(t) = self.active.get(id) {
return SubagentProvenance {
@@ -254,7 +250,6 @@ impl SubagentCoordinator {
model_id: meta.effective_model_id,
})
}
/// Check whether an ID refers to a currently-active (running) subagent.
pub(crate) fn is_active(&self, id: &str) -> bool {
self.active.contains_key(id)
}
@@ -294,7 +289,6 @@ impl SubagentCoordinator {
}
/// Snapshot all currently-running subagents for compaction state context.
///
/// Returns one `ActiveSubagentSummary` per entry in the `active` map.
/// Completed/failed/cancelled subagents are NOT included — they live in
/// the `completed` map and are irrelevant for post-compaction reminders
/// (the model already saw their tool results before compaction).
@@ -36,11 +36,6 @@ pub(super) fn task_model_override_error(
is_session_auth,
)
}
/// This is a free async function, NOT a method on MvpAgent. It receives
/// a `SubagentSpawnContext` with everything it needs, and a mutable
/// reference to the coordinator for tracking.
///
/// Returns when the child session completes (or fails/cancels).
#[tracing::instrument(
name = "subagent.handle_request",
skip_all,
@@ -43,7 +43,7 @@ pub(crate) enum InitialContextSource {
New,
/// Parent history as `<background_context>` (harness-only chat-prefix fork).
Forked,
/// Resumed from a previously completed peer subagent. The child inherits
/// Resumed from a completed peer subagent. The child inherits
/// the source's raw transcript, tool state, and model. System prompt and
/// prompt context are freshly rendered from the current agent definition.
Resumed,
@@ -211,7 +211,6 @@ pub(crate) struct SubagentSpawnContext {
/// Resolved config for the deploy service.
pub app_builder_deployer_config:
kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig,
/// Whether the write_file tool is enabled.
pub write_file_enabled: bool,
/// Whether goal mode (`/goal`) is enabled.
pub goal_enabled: bool,
@@ -423,7 +422,7 @@ impl SubagentSpawnContext {
/// session-level config, so it is resolved from the same tiers as the
/// parent (requirements/env/user/managed from disk; remote from the
/// parent's snapshot) and follows the session into subagents. Bash stays
/// on tool defaults, as before that knob existed.
/// on tool defaults.
pub fn resolve_tool_params_json(
&self,
) -> crate::session::agent_rebuild::ResolvedToolParamsJson {
@@ -619,7 +618,6 @@ pub(crate) async fn resolve_snapshot(lookup: Option<SnapshotLookup>) -> Option<S
}
}
}
/// Check whether a resolved snapshot is still in the `Running` state.
pub(crate) fn is_running(snap: &SubagentSnapshot) -> bool {
matches!(
snap.status,
@@ -2208,7 +2206,6 @@ fn emit_subagent_notification(
gateway.forward_fire_and_forget(ext_notification);
}
}
/// Progress notification emission interval.
const PROGRESS_PUBLISH_INTERVAL: std::time::Duration = std::time::Duration::from_secs(2);
/// Change signature for the progress-publisher dedupe:
/// `(turn_count, tool_call_count, context_usage_pct, error_count, tokens_used)`.
@@ -126,8 +126,6 @@ fn end_to_end_normalized_conversation_shape() {
assert_eq!(prefix_len, 2);
assert!(prefix_len < conv.len(), "prefix should not cover the task");
}
/// Verify that the task prompt (not background context) would be the
/// cached prompt text in the session pipeline.
#[test]
fn cached_prompt_text_is_task_not_background() {
use kigi_sampling_types::conversation::ConversationItem;
@@ -162,7 +160,6 @@ fn cached_prompt_text_is_task_not_background() {
"background should be the inherited context"
);
}
/// Verify extract_last_real_user_query would return the task.
#[test]
fn last_user_message_is_task_after_normalization() {
use kigi_sampling_types::conversation::ConversationItem;
@@ -286,7 +283,6 @@ fn compaction_preserves_inherited_prefix() {
bg_count, 1, "should have exactly one background_context after compaction"
);
}
/// Verify that compaction with prefix_len=0 (non-forked) passes through unchanged.
#[test]
fn compaction_no_prefix_passes_through() {
use kigi_sampling_types::conversation::ConversationItem;
@@ -623,7 +619,6 @@ fn subagent_worktree_snapshot_gate_defaults_off() {
let ctx = ctx_with_toggle(std::collections::HashMap::new());
assert!(! ctx.resolve_subagent_worktree_snapshot_enabled());
}
/// Remote remote settings value enables the gate when no local override exists.
#[test]
fn subagent_worktree_snapshot_gate_remote_enables() {
let mut ctx = ctx_with_toggle(std::collections::HashMap::new());
@@ -649,7 +644,6 @@ fn subagent_worktree_snapshot_gate_local_overrides_remote() {
"local [features] subagent_worktree_snapshot=false must override remote enable"
);
}
/// Local config alone enables the gate (the per-deployment rollout lever).
#[test]
fn subagent_worktree_snapshot_gate_local_enables() {
let mut config = crate::agent::config::Config::default();
@@ -2380,7 +2374,7 @@ async fn resolve_subagent_inherits_parent_model_without_pins() {
/// An explicit `[subagents.models]` pin routes the subagent to that
/// model regardless of the parent model — both a light parent
/// (`kigi-4.5`) and a custom parent (`composer-2-fast`)
/// honor the pin identically now that the heavy-model gate is gone.
/// honor the pin identically.
#[tokio::test]
async fn resolve_subagent_config_override_pin_applies_for_any_parent() {
use kigi_agent::config::ModelOverride;
@@ -2541,9 +2535,7 @@ async fn subagent_override_first_party_model_still_gets_primary_token() {
/// LEAK guard (C1, subagent-override `api_key` channel): an API-key registry
/// platform override must NOT receive the parent's primary Kimi session token —
/// `resolve_credentials` would stamp it as the child's `api_key` on
/// `api.moonshot.cn`. This test previously asserted the opposite
/// (`subagent_override_non_oauth_model_still_gets_primary_token`), which encoded
/// the defect.
/// `api.moonshot.cn`.
///
/// Revert-to-red: make `CredentialAuthority::governing_manager`'s
/// `Some(platform) => None` arm return `self.primary.clone()` and `api_key`
@@ -5,17 +5,14 @@ use serde_json;
use crate::session::replay_events::SessionNotification;
/// Controls how sampling/output chunks are buffered before being delivered to the client.
/// Parsed from `InitializeRequest.meta.bufferingSettings` and preserved for later use.
/// Parsed from `InitializeRequest.meta.bufferingSettings`.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub(crate) struct BufferingSettings {
/// Maximum number of items to accumulate before flushing
#[serde(default = "default_max_items")]
pub max_items: u64,
/// Maximum total bytes to accumulate before flushing
#[serde(default = "default_max_bytes")]
pub max_bytes: u64,
/// Maximum time in milliseconds to wait before flushing buffered items
#[serde(default = "default_max_duration_ms")]
pub max_duration_ms: u64,
}
@@ -25,20 +22,15 @@ fn default_max_items() -> u64 {
}
fn default_max_bytes() -> u64 {
1024 * 2 // 2 KB
1024 * 2
}
fn default_max_duration_ms() -> u64 {
10 // 10 ms
10
}
/// Low-level buffer for ACP text chunks (agent message/thought chunks).
///
/// API:
/// - `consume_chunk(...) -> Option<SessionNotification>` returns a notification that should be sent now
/// (typically the previously buffered one), or `None` if we keep buffering.
/// - `flush() -> Option<SessionNotification>` returns any pending buffered notification to send.
///
/// Example of session notification:
/// ```json
/// {
@@ -133,7 +125,6 @@ impl ReplayBuffer {
// can't merge, we need to send both chunks immediately to preserve current chunk order.
match self.pending.take() {
Some(pending) => {
// No buffered item after this call.
self.pending_count = 0;
self.pending_bytes = 0;
return Some((pending, Some(incoming)));
@@ -145,7 +136,7 @@ impl ReplayBuffer {
}
if !incoming_notification_timestamp_in_range {
// need to pop previously pending notification and send it immediately
// need to pop the pending notification and send it immediately
let prev = self.pending.replace(incoming);
{
let prev = prev?;
@@ -156,7 +147,6 @@ impl ReplayBuffer {
let pending = self.pending.take();
let had_prev = pending.is_some();
let prev_count = self.pending_count;
// pending is now empty; we'll either refill it (and set counts) or send immediately.
self.pending_count = 0;
self.pending_bytes = 0;
@@ -165,7 +155,6 @@ impl ReplayBuffer {
match (force_send, first, second) {
(_, pending, Some(next)) => {
// Merge wasn't allowed; send both immediately to preserve current chunk order.
// (Nothing remains buffered.)
self.pending_count = 0;
self.pending_bytes = 0;
Some((pending, Some(next)))
@@ -221,10 +210,7 @@ impl ReplayBuffer {
second.map(|s| SessionNotification::Xai(Box::new(s))),
)
}
// Different kinds: can't merge. Force-flush prev, return incoming as second.
(Some(prev), incoming) => (true, prev, Some(incoming)),
// No pending: buffer if the new chunk is a streaming kind,
// force-send otherwise (e.g. ToolCall, Plan, etc.).
(None, incoming) => {
let bufferable = incoming.is_streaming_chunk();
(!bufferable, incoming, None)
@@ -974,7 +960,7 @@ mod tests {
assert!(replay_buffer.flush().is_none());
}
// ── Xai / ToolCallDeltaChunk tests ──────────────────────────────
// Xai / ToolCallDeltaChunk tests
fn delta_chunk(
session: &str,