M0: compilable skeleton — Kigi 0.1.0 fork surgery

Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
@@ -0,0 +1,485 @@
//! Commands sent to the ChatStateActor.
use std::collections::BTreeSet;
use kigi_sampling_types::{
ConversationItem, ConversationRequest, DanglingToolCallReason, SamplingConfig, TokenUsage,
ToolSpec, TraceContext,
};
use tokio::sync::oneshot;
use crate::types::{
AutoCompactTrigger, ChatStateSnapshot, ConversationCounts, Credentials, NotificationMeta,
TurnCapture,
};
#[derive(Debug, Clone, Default)]
pub struct ModelMetadata {
pub resolved_model_id: Option<String>,
pub model_fingerprint: Option<String>,
}
/// Refusal reply for [`ChatStateCommand::RepairHistory`]: a turn was in
/// flight, and in-flight tool calls must not be treated as dangling.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RepairHistoryBlocked;
impl std::fmt::Display for RepairHistoryBlocked {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"cannot repair history while a turn is in flight; stop the turn first"
)
}
}
impl std::error::Error for RepairHistoryBlocked {}
/// Commands sent to the ChatStateActor via mpsc channel.
pub enum ChatStateCommand {
// ═══ Mutations (fire-and-forget) ═══
/// Push a user message into the conversation.
PushUserMessage { item: ConversationItem },
/// Push a user message and acknowledge once the chat-state actor has
/// accepted and processed it.
PushUserMessageAndAck {
item: ConversationItem,
reply: oneshot::Sender<()>,
},
/// Push a user message with an explicit dangling-repair reason.
PushUserMessageWithRepairReason {
item: ConversationItem,
reason: DanglingToolCallReason,
},
/// Record the assistant's response (text + tool calls).
PushAssistantResponse { item: ConversationItem },
/// Record a tool result.
PushToolResult { item: ConversationItem },
/// Record accumulated token usage from a streaming response.
RecordTokenUsage { total_tokens: u64 },
/// Stash the per-turn `TokenUsage` from the most recent model response.
/// Overwrites any previously stashed value.
RecordLastTurnUsage { usage: TokenUsage },
RecordModelCallUsage {
model_id: Option<String>,
usage: TokenUsage,
api_duration_ms: Option<u64>,
cost_usd_ticks: Option<i64>,
},
/// Subagent usage into session (and prompt when attributable). Replies when applied.
RecordSubagentUsage {
by_model: Vec<(String, crate::usage::UsageTotals)>,
attribute_to_prompt: bool,
/// Nested subagent bill may under-count.
incomplete: bool,
reply: oneshot::Sender<()>,
},
/// Mark open prompt and/or session ledgers incomplete.
MarkUsageIncomplete {
prompt: bool,
session: bool,
reply: oneshot::Sender<()>,
},
/// Increment prompt_index (called at start of each user turn).
IncrementPromptIndex,
/// Update the sampling config (e.g., model switch).
UpdateSamplingConfig { config: SamplingConfig },
/// Track that the agent edited a file path.
RecordAgentEditedPath { path: String },
/// Record stream timing metadata.
RecordStreamStart { timestamp_ms: i64 },
/// Record turn timing metadata.
RecordTurnStart { timestamp_ms: i64 },
/// Replace conversation history.
ReplaceConversation {
items: Vec<ConversationItem>,
is_compaction: bool,
},
/// Out-of-band history repair (`x.ai/session/repair`): run
/// [`crate::compaction_utils::repair_history`] and persist when changed;
/// `dry_run` only reports.
///
/// `turn_active` (the session's shared flag, set at turn start BEFORE the
/// turn pushes anything here) is re-checked inside the command handler:
/// a caller-side check alone races turn start, whereas at processing time
/// the command is either refused or runs on pre-turn state with the
/// turn's pushes serialized after it.
RepairHistory {
dry_run: bool,
turn_active: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
reply: oneshot::Sender<
Result<crate::compaction_utils::HistoryRepairReport, RepairHistoryBlocked>,
>,
},
/// Atomically align the leading `System` message with `prompt` (inserting
/// one if absent), persisting the conversation. Executed inside the actor so
/// it serializes with concurrent turn pushes (`PushAssistantResponse` /
/// `PushToolResult`) — a mid-turn reconnect cannot lose those updates the
/// way a read-modify-write via `GetConversation` + `ReplaceConversation`
/// would. Replies `true` iff the conversation changed (no-op when the head
/// already matches modulo trailing newlines). A changed head goes through
/// `replace_conversation`, which re-bases `total_tokens` to a fresh static
/// estimate — acceptable because a changed head invalidates the KV prefix
/// anyway.
ReplaceSystemHead {
prompt: String,
reply: oneshot::Sender<bool>,
},
/// Cache prompt text for rewind preview.
CachePromptText { text: String },
/// Record compaction boundary for rewind.
RecordCompactionAt { prompt_index: usize },
/// Flush pending persistence writes to disk (end of turn).
Flush,
/// Update opaque credential secrets held by the actor.
UpdateCredentials { credentials: Credentials },
/// Restore from a snapshot.
RestoreSnapshot(Box<ChatStateSnapshot>),
/// Start capturing turn messages. Clears any previous buffer.
BeginTurnCapture,
/// Append synthetic `task` pairs for a harness-spawned subagent (goal
/// planner / verifier skeptic) to the in-progress harness trace phase.
/// Accumulated independently of the live `conversation` and of
/// `turn_capture`; sealed into a standalone trace turn by
/// `FlushHarnessTraceTurn`.
AppendHarnessTraceItems { items: Vec<ConversationItem> },
/// Seal the harness items accumulated since the last flush into one
/// standalone trace turn. Issued once per harness phase (after the planner,
/// after each verifier panel). No-op when nothing was recorded.
FlushHarnessTraceTurn,
/// Repair dangling tool calls after a harness-initiated halt.
RepairDanglingAfterHarnessHalt { class: &'static str },
// ═══ Queries (request/response via oneshot) ═══
/// Build a ConversationRequest ready to send to the API.
/// Clones the conversation, prunes old tool results, repairs dangling
/// tool calls, injects memory reminder, and assembles the request.
BuildConversationRequest {
tool_definitions: Vec<ToolSpec>,
memory_reminder: Option<String>,
persist_memory_reminder: bool,
trace: Option<Box<dyn TraceContext>>,
conv_id: String,
req_id: String,
reply: oneshot::Sender<ConversationRequest>,
},
/// Get a clone of the full conversation.
GetConversation {
reply: oneshot::Sender<Vec<ConversationItem>>,
},
/// Get current prompt index.
GetPromptIndex { reply: oneshot::Sender<usize> },
/// Get the prompt index at which the last compaction occurred.
/// `Some` means the context currently holds a compaction summary.
GetLastCompactionPromptIndex {
reply: oneshot::Sender<Option<usize>>,
},
/// Get total accumulated tokens.
GetTotalTokens { reply: oneshot::Sender<u64> },
/// Retrieve the most recent stashed per-turn `TokenUsage`. Returns
/// `None` until at least one `RecordLastTurnUsage` has been processed.
GetLastTurnUsage {
reply: oneshot::Sender<Option<TokenUsage>>,
},
GetPromptUsage {
reply: oneshot::Sender<Option<crate::usage::UsageLedger>>,
},
GetSessionUsage {
reply: oneshot::Sender<crate::usage::UsageLedger>,
},
/// `total_tokens` + bytes/4 delta from tool results since last model response.
GetEstimatedTotalTokens { reply: oneshot::Sender<u64> },
/// Bytes/4 estimate of all non-system conversation items.
GetEstimatedMessagesTokens { reply: oneshot::Sender<u64> },
/// Get sampling config.
GetSamplingConfig {
reply: oneshot::Sender<SamplingConfig>,
},
/// Get the set of agent-edited file paths.
GetAgentEditedPaths {
reply: oneshot::Sender<BTreeSet<String>>,
},
/// Get notification meta (timing info).
GetNotificationMeta {
reply: oneshot::Sender<NotificationMeta>,
},
/// Snapshot state for forking or rewind.
Snapshot {
reply: oneshot::Sender<ChatStateSnapshot>,
},
/// Truncate conversation to a target prompt index (for rewind).
TruncateToPromptIndex {
target_prompt_index: usize,
reply: oneshot::Sender<()>,
},
/// Check if auto-compact is needed (returns token info).
CheckAutoCompactNeeded {
threshold_percent: u8,
reply: oneshot::Sender<Option<AutoCompactTrigger>>,
},
/// Get credential secrets.
GetCredentials { reply: oneshot::Sender<Credentials> },
GetLastModelMetadata {
reply: oneshot::Sender<ModelMetadata>,
},
/// Take the accumulated turn messages and end the capture.
/// Returns `None` if no capture was active.
TakeTurnMessages {
reply: oneshot::Sender<Option<TurnCapture>>,
},
/// Drain the sealed harness trace turns (goal planner + verifier panels).
/// Each `Vec` is one turn's synthetic `task` pairs, uploaded by the agent
/// as its own sibling `turn_{N}` artifact. Seals a trailing un-flushed
/// accumulator before draining.
TakeHarnessTraceTurns {
reply: oneshot::Sender<Vec<Vec<ConversationItem>>>,
},
// ═══ Narrow targeted queries (avoid full-conversation clone) ═══
/// Get the number of items in the conversation.
/// Cheaper than `GetConversation` when only the length is needed.
GetConversationLen { reply: oneshot::Sender<usize> },
/// Whether any assistant tool call lacks a matching `ToolResult` (i.e. the
/// dangling-tool-call repair would fire on the next request build).
/// Cheaper than `GetConversation` when only this predicate is needed.
HasDanglingToolCalls { reply: oneshot::Sender<bool> },
/// Get the text content of the last assistant message with non-empty text.
/// Returns `None` if no such message exists.
/// Cheaper than `GetConversation` when only the final assistant response is needed.
GetLastAssistantText {
reply: oneshot::Sender<Option<String>>,
},
/// Get the text of the first `Text` content part in the first `User` message.
/// Returns `None` if the conversation has no user messages or the first user
/// message has no text content part.
/// Cheaper than `GetConversation` when only the initial user query is needed.
GetFirstUserText {
reply: oneshot::Sender<Option<String>>,
},
/// Get a single conversation item by index (0-based).
/// Returns `None` if the index is out of bounds.
/// Cheaper than `GetConversation` when only one item is needed.
GetConversationItemAt {
index: usize,
reply: oneshot::Sender<Option<ConversationItem>>,
},
/// Get the processed text of the last user query (metadata tags stripped).
///
/// Equivalent to `extract_last_user_query(&conversation)` but without
/// cloning the full conversation on the caller side.
GetLastUserQueryText {
reply: oneshot::Sender<Option<String>>,
},
/// Get item counts for the conversation by role.
///
/// Returns a `ConversationCounts` struct without cloning any items.
/// Suitable for telemetry / logging that only needs totals.
GetConversationCounts {
reply: oneshot::Sender<ConversationCounts>,
},
/// Get the first `System` message in the conversation, if any.
///
/// Cheaper than `GetConversation` when only the system prompt is needed
/// (e.g. for compaction setup or error guards).
GetSystemMessage {
reply: oneshot::Sender<Option<ConversationItem>>,
},
}
#[cfg(test)]
mod tests {
use super::*;
/// Verify that every command variant is constructible (compile-time check).
#[test]
fn command_variants_are_constructible() {
// Mutations
let _ = ChatStateCommand::PushUserMessage {
item: ConversationItem::user("hello"),
};
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::PushUserMessageAndAck {
item: ConversationItem::user("hello"),
reply: tx,
};
let _ = ChatStateCommand::PushAssistantResponse {
item: ConversationItem::assistant("hi"),
};
let _ = ChatStateCommand::PushToolResult {
item: ConversationItem::tool_result("call-1", "result"),
};
let _ = ChatStateCommand::RecordTokenUsage { total_tokens: 100 };
let _ = ChatStateCommand::IncrementPromptIndex;
let _ = ChatStateCommand::UpdateSamplingConfig {
config: SamplingConfig {
base_url: String::new(),
model: String::new(),
max_completion_tokens: None,
temperature: None,
top_p: None,
api_backend: Default::default(),
extra_headers: Default::default(),
context_window: std::num::NonZeroU64::new(128_000).unwrap(),
reasoning_effort: None,
stream_tool_calls: None,
},
};
let _ = ChatStateCommand::RecordAgentEditedPath {
path: "src/main.rs".to_string(),
};
let _ = ChatStateCommand::RecordStreamStart {
timestamp_ms: 12345,
};
let _ = ChatStateCommand::RecordTurnStart {
timestamp_ms: 12345,
};
let _ = ChatStateCommand::ReplaceConversation {
items: vec![],
is_compaction: false,
};
let _ = ChatStateCommand::CachePromptText {
text: "prompt".to_string(),
};
let _ = ChatStateCommand::RecordCompactionAt { prompt_index: 0 };
let _ = ChatStateCommand::Flush;
// Queries
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetConversation { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetPromptIndex { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetLastCompactionPromptIndex { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetTotalTokens { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetEstimatedTotalTokens { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetSamplingConfig { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetAgentEditedPaths { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::BuildConversationRequest {
tool_definitions: vec![],
memory_reminder: None,
persist_memory_reminder: false,
trace: None,
conv_id: String::new(),
req_id: String::new(),
reply: tx,
};
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetNotificationMeta { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::Snapshot { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::TruncateToPromptIndex {
target_prompt_index: 0,
reply: tx,
};
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::CheckAutoCompactNeeded {
threshold_percent: 85,
reply: tx,
};
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetLastModelMetadata { reply: tx };
let _ = ChatStateCommand::BeginTurnCapture;
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::TakeTurnMessages { reply: tx };
// Narrow targeted queries
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetConversationLen { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetLastAssistantText { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetFirstUserText { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetConversationItemAt {
index: 0,
reply: tx,
};
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetLastUserQueryText { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetConversationCounts { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetSystemMessage { reply: tx };
let (tx, _rx) = oneshot::channel();
let _ = ChatStateCommand::GetEstimatedMessagesTokens { reply: tx };
}
}