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,229 @@
//! Per-request transport for server-reported doom-loop signals.
//!
//! The wire shapes and tolerant parsers live in
//! [`kigi_sampling_types::doom_loop`]; this module only moves the parsed
//! signals across the layer boundary: the Layer-1 SSE decoder in
//! [`crate::client`] records them as raw payloads arrive, and the Layer-2
//! transform in [`crate::stream::responses`] drains them into the final
//! `ConversationResponse`.
use std::sync::{Arc, Mutex};
use kigi_sampling_types::doom_loop::{
DOOM_LOOP_CHECK_EVENT_TYPE, DoomLoopPeek, DoomLoopRecoveryPolicy, DoomLoopSignal,
peek_doom_loop,
};
/// Cheap-to-clone accumulator shared between the SSE decode closure and the
/// stream transform of one request attempt. Created fresh per attempt so
/// signals from a failed attempt can never leak into the next one. Carries
/// the policy so the stream transform can judge confidence for the
/// mid-stream abort; the retry loop disarms the abort once the recovery
/// budget is spent so the final attempt completes and can be accepted.
#[derive(Clone, Debug, Default)]
pub struct DoomLoopSignalCollector {
inner: Arc<Mutex<CollectorState>>,
}
#[derive(Debug, Default)]
struct CollectorState {
signals: Vec<DoomLoopSignal>,
malformed_logged: bool,
policy: DoomLoopRecoveryPolicy,
// Inverted so `derive(Default)` starts attempts armed.
abort_disarmed: bool,
}
impl DoomLoopSignalCollector {
/// A fresh, armed collector judging confidence with `policy`.
pub(crate) fn new(policy: DoomLoopRecoveryPolicy) -> Self {
let collector = Self::default();
if let Ok(mut state) = collector.inner.lock() {
state.policy = policy;
}
collector
}
/// Stop the mid-stream abort for this attempt; signals keep recording.
pub(crate) fn disarm_abort(&self) {
if let Ok(mut state) = self.inner.lock() {
state.abort_disarmed = true;
}
}
/// While armed: the raw labels of the confident signals recorded so far
/// (non-draining), or `None` when there is nothing to act on.
pub(crate) fn abort_triggers(&self) -> Option<Vec<String>> {
let state = self.inner.lock().ok()?;
if state.abort_disarmed {
return None;
}
let confident = state.policy.confident_triggers(&state.signals);
(!confident.is_empty()).then_some(confident)
}
/// Inspect a raw SSE frame. Returns `true` when the frame is the
/// non-standard `response.doom_loop_check` event — by its SSE `event:`
/// name or its payload `type` — which the caller must swallow;
/// forwarding it would fail typed deserialization. Reported triggers
/// (mid-stream or on the terminal response object) are recorded,
/// deduplicated by raw label. Never fails.
pub(crate) fn absorb(&self, event_name: &str, data: &str) -> bool {
// The name check keeps a check event with an unparseable payload
// from ever reaching the typed parser.
let named = event_name == DOOM_LOOP_CHECK_EVENT_TYPE;
let (signals, swallow) = match peek_doom_loop(data) {
DoomLoopPeek::CheckEvent(signals) => (signals, true),
DoomLoopPeek::ResponseField(signals) => (signals, false),
DoomLoopPeek::None => {
if named {
self.log_malformed_once();
}
return named;
}
};
if signals.is_empty() {
self.log_malformed_once();
} else {
self.record(signals);
}
swallow || named
}
/// Drain the recorded signals; empty when nothing was reported.
pub(crate) fn take(&self) -> Vec<DoomLoopSignal> {
match self.inner.lock() {
Ok(mut state) => std::mem::take(&mut state.signals),
Err(_) => Vec::new(),
}
}
fn record(&self, signals: Vec<DoomLoopSignal>) {
let Ok(mut state) = self.inner.lock() else {
return;
};
// Cumulative sets are re-sent as they grow; the raw label is the
// stable identity. Linear scan is fine for these tiny sets.
for signal in signals {
if !state.signals.iter().any(|s| s.raw == signal.raw) {
state.signals.push(signal);
}
}
}
/// Debug-log the first malformed payload per attempt (never per event).
fn log_malformed_once(&self) {
let Ok(mut state) = self.inner.lock() else {
return;
};
if !state.malformed_logged {
state.malformed_logged = true;
tracing::debug!("doom-loop check payload malformed or empty; ignoring");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use kigi_sampling_types::doom_loop::{
DoomLoopSignalKind, SAMPLE_CHECK_EVENT_DATA, SAMPLE_CHECK_EVENT_DATA_CUMULATIVE,
};
#[test]
fn absorb_swallows_check_event_and_records_signals() {
let collector = DoomLoopSignalCollector::default();
assert!(collector.absorb(DOOM_LOOP_CHECK_EVENT_TYPE, SAMPLE_CHECK_EVENT_DATA));
let signals = collector.take();
assert_eq!(signals.len(), 1);
assert_eq!(signals[0].kind, DoomLoopSignalKind::TailRepetition(4));
}
/// Servers that omit the SSE `event:` name are still handled by the
/// payload `type` check.
#[test]
fn absorb_swallows_check_event_without_sse_name() {
let collector = DoomLoopSignalCollector::default();
assert!(collector.absorb("message", SAMPLE_CHECK_EVENT_DATA));
assert_eq!(collector.take().len(), 1);
}
#[test]
fn absorb_dedupes_cumulative_sets_by_raw_label() {
let collector = DoomLoopSignalCollector::default();
assert!(collector.absorb(DOOM_LOOP_CHECK_EVENT_TYPE, SAMPLE_CHECK_EVENT_DATA));
assert!(collector.absorb(
DOOM_LOOP_CHECK_EVENT_TYPE,
SAMPLE_CHECK_EVENT_DATA_CUMULATIVE
));
let signals = collector.take();
assert_eq!(signals.len(), 2);
assert_eq!(signals[0].raw, "tail_repetition:4@response");
assert_eq!(signals[1].raw, "tail_repetition:2@response");
}
#[test]
fn absorb_forwards_ordinary_and_terminal_payloads() {
let collector = DoomLoopSignalCollector::default();
let delta = r#"{"type":"response.output_text.delta","delta":"hi"}"#;
assert!(!collector.absorb("response.output_text.delta", delta));
assert!(collector.take().is_empty());
// Terminal response field is recorded but the event is forwarded.
let terminal = r#"{"type":"response.completed","response":{"id":"r1","doom_loop_check":{"triggers":["low_logprob@response"]}}}"#;
assert!(!collector.absorb("response.completed", terminal));
assert_eq!(collector.take().len(), 1);
}
#[test]
fn malformed_check_event_swallowed_without_signals() {
let collector = DoomLoopSignalCollector::default();
let wrong_type =
r#"{"type":"response.doom_loop_check","doom_loop_check":{"triggers":"nope"}}"#;
assert!(collector.absorb(DOOM_LOOP_CHECK_EVENT_TYPE, wrong_type));
assert!(collector.absorb("message", r#"{"type":"response.doom_loop_check"}"#));
assert!(collector.take().is_empty());
}
/// A frame with the check event's SSE name but an unparseable payload
/// (non-JSON, or JSON without the `type` tag) must still be swallowed —
/// forwarding it would fail the typed parse and the whole attempt.
#[test]
fn named_event_with_garbage_payload_still_swallowed() {
let collector = DoomLoopSignalCollector::default();
assert!(collector.absorb(DOOM_LOOP_CHECK_EVENT_TYPE, "not json at all"));
assert!(collector.absorb(DOOM_LOOP_CHECK_EVENT_TYPE, r#"{"no_type_tag":true}"#));
assert!(collector.take().is_empty());
}
#[test]
fn take_drains_once() {
let collector = DoomLoopSignalCollector::default();
collector.absorb(DOOM_LOOP_CHECK_EVENT_TYPE, SAMPLE_CHECK_EVENT_DATA);
assert!(!collector.take().is_empty());
assert!(collector.take().is_empty());
}
/// `abort_triggers` fires only on confident signals, does not drain, and
/// goes quiet once disarmed (the spent-budget attempt must complete).
#[test]
fn abort_triggers_requires_confidence_and_honors_disarm() {
let confident = r#"{"type":"response.doom_loop_check","doom_loop_check":{"triggers":["tail_repetition:8@thinking"]}}"#;
let collector = DoomLoopSignalCollector::new(DoomLoopRecoveryPolicy::default());
// Non-confident channel: recorded but not actionable.
assert!(collector.absorb(DOOM_LOOP_CHECK_EVENT_TYPE, SAMPLE_CHECK_EVENT_DATA));
assert!(collector.abort_triggers().is_none());
assert!(collector.absorb(DOOM_LOOP_CHECK_EVENT_TYPE, confident));
assert_eq!(
collector.abort_triggers(),
Some(vec!["tail_repetition:8@thinking".to_string()])
);
// Non-draining: probing twice and taking afterwards both work.
assert!(collector.abort_triggers().is_some());
collector.disarm_abort();
assert!(collector.abort_triggers().is_none());
assert_eq!(collector.take().len(), 2, "recording survives the disarm");
}
}