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,222 @@
//! Allocator memory-release seam.
//!
//! Resuming a large session parses the whole `updates.jsonl` and stages the
//! replay in memory — a multi-hundred-MB transient for big sessions. The
//! allocations are freed once replay completes, but with jemalloc the freed
//! pages stay attached to the process (macOS additionally counts `MADV_FREE`d
//! pages in RSS until real memory pressure), so an idle, just-resumed TUI
//! looks like it is "using" several times its live heap.
//!
//! The pager library cannot reference jemalloc (the allocator choice belongs
//! to the composition-root binary — see `kigi-bin/src/main.rs`), so
//! the binary installs a release hook here at startup, mirroring the
//! `minimal_hook` IoC seam. The hook purges retained arena pages
//! (`arena.<ALL>.purge`); calling it right after heavy transients drop returns
//! that memory to the OS instead of letting it linger for the process
//! lifetime.
//!
//! Absent a hook (tests, alternate binaries, non-jemalloc builds) everything
//! here is inert.
//!
//! ## Covered memory cliffs
//!
//! [`release_retained_memory`] is invoked after every known multi-MB drop:
//!
//! - session-load replay completion (`dispatch/session/load.rs`) — the parsed
//! `updates.jsonl` staging transient (100s of MB for long sessions)
//! - reconnect-reload window finalize/abort/supersede
//! (`agent_view/session.rs`) — gated on `apply_reload_outcome` reporting a
//! heavy drop: the stashed pre-reload `ScrollbackState` (full-replay
//! success) or the discarded staging (failure). The common cursor-resolve
//! outcome reuses the stash and does NOT purge.
//! - subagent transcript replay (`app/subagent.rs`,
//! `replay_inherited_updates`) — covers both producers (eager live-spawn
//! and resume-deferred first open), gated on a non-empty replay
//! - closing an agent tab (`dispatch/session/modal.rs`) — the whole
//! `AgentView` incl. scrollback, render caches, and child views
//! - video teardown — the pre-extracted frame set (~50300 MB per video):
//! viewer close (`media.rs`, synchronous — input path), inline stop /
//! scroll-off / replacement (`media.rs`, `render.rs`, `app_view.rs` —
//! [`request_release_after_draw`], the purge runs post-frame-flush)
//! - image viewer close (`media.rs`) — the decoded overlay image
//! - rewind truncation (`dispatch/rewind.rs`) — the removed transcript tail
//!
//! Every purge is edge-triggered by a rare lifecycle/user event and gated on
//! an actual drop, never per frame; draw/tick-path cliffs defer the purge to
//! the post-flush gap so it cannot stall the frame being painted.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
static RELEASE_HOOK: OnceLock<fn()> = OnceLock::new();
/// Install the allocator release hook. Idempotent; first caller wins.
/// Called once by the composition-root binary before the app runs.
pub fn install_release_hook(hook: fn()) {
let _ = RELEASE_HOOK.set(hook);
}
/// Ask the allocator to return freed-but-retained pages to the OS.
///
/// Cheap enough to call after any known memory cliff (see the module docs for
/// the covered sites). No-op when no hook is installed.
///
/// Call this directly from dispatch/input paths (the stall lands between
/// interactions). From draw/tick paths use [`request_release_after_draw`]
/// instead: when a purge has real work to do (a ~50300 MB frame set just
/// dropped) the synchronous madvise would land inside the very frame the
/// user is waiting for.
pub fn release_retained_memory() {
release_retained_memory_with("unattributed");
}
/// [`release_retained_memory`] with memory-cliff attribution: `reason` tags
/// the purge event in the memory trace (`memory_trace` module) so per-site
/// purge frequency, duration, and released footprint are analyzable offline.
/// Use a short stable kebab-case tag (e.g. `"session-load-replay"`).
pub fn release_retained_memory_with(reason: &'static str) {
let hook = RELEASE_HOOK.get();
// Skip gauge sampling entirely when tracing is off (`KIGI_MEMTRACE=0`
// or no sink): a disabled trace must add zero syscalls to purges.
let trace = crate::memory_trace::is_active();
let before = if trace {
// Same gauge precedence as the trace's threshold logic: physical
// footprint where available (macOS), else RSS (Linux) — so purge
// deltas are computable on every platform.
let mem = crate::memory_trace::sample_process_memory();
mem.footprint_bytes.or(mem.rss_bytes)
} else {
None
};
let started = std::time::Instant::now();
if let Some(hook) = hook {
hook();
}
if trace {
crate::memory_trace::record_purge(reason, hook.is_some(), before, started.elapsed());
}
}
/// Deferred-release request flag, drained post-frame-flush.
///
/// `AtomicBool` rather than a thread-local: requesters (draw/tick code) and
/// the drainer (`AppView::draw` tail) are both main-thread today, but the
/// flag must not silently drop a request if that ever changes.
static RELEASE_AFTER_DRAW: AtomicBool = AtomicBool::new(false);
/// Memory-cliff tag for the pending deferred request. Coalescing requests
/// keep the LAST writer's reason — precise enough for trace attribution
/// (coalesced requests within one frame are the same user gesture).
static DEFER_REASON: Mutex<&'static str> = Mutex::new("post-draw");
/// Request [`release_retained_memory`] to run right after the current frame
/// flushes (drained by [`run_deferred_release`] at the end of
/// `AppView::draw`). For memory cliffs hit *inside* the draw/tick path —
/// e.g. inline video stopped because it scrolled off screen.
pub fn request_release_after_draw() {
request_release_after_draw_with("post-draw");
}
/// [`request_release_after_draw`] with memory-cliff attribution for the
/// trace (see [`release_retained_memory_with`]).
pub fn request_release_after_draw_with(reason: &'static str) {
if let Ok(mut r) = DEFER_REASON.lock() {
*r = reason;
}
RELEASE_AFTER_DRAW.store(true, Ordering::Relaxed);
}
/// Drain a pending [`request_release_after_draw`], if any. Called once at
/// the end of `AppView::draw`, after the terminal buffer flush, so the purge
/// cost lands in the idle gap between frames instead of inside one.
pub fn run_deferred_release() {
if RELEASE_AFTER_DRAW.swap(false, Ordering::Relaxed) {
let reason = DEFER_REASON.lock().map(|r| *r).unwrap_or("post-draw");
release_retained_memory_with(reason);
}
}
/// Test support: a counting release hook with a **per-thread** counter.
///
/// The real `RELEASE_HOOK` is a process-global `OnceLock`, but dispatch/view
/// code always calls [`release_retained_memory`] on the calling thread, so a
/// thread-local count lets parallel `cargo test` threads assert both positive
/// ("this path released") and negative ("this path must not release") deltas
/// without cross-test interference.
#[cfg(test)]
pub(crate) mod test_support {
use std::cell::Cell;
thread_local! {
static CALLS: Cell<usize> = const { Cell::new(0) };
}
fn counting_hook() {
CALLS.with(|c| c.set(c.get() + 1));
}
/// Install the counting hook (idempotent; first caller wins, and every
/// test installs the same `fn`, so ordering does not matter).
pub(crate) fn install_counting_hook() {
super::install_release_hook(counting_hook);
}
/// Number of releases observed **on this thread**.
pub(crate) fn calls() -> usize {
CALLS.with(|c| c.get())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn release_invokes_installed_hook_per_call() {
// The hook slot is a global OnceLock shared with sibling tests, but
// the counter is thread-local, so deltas on THIS thread are exact.
test_support::install_counting_hook();
let before = test_support::calls();
release_retained_memory();
release_retained_memory();
assert_eq!(
test_support::calls(),
before + 2,
"each release must invoke the installed hook exactly once"
);
}
/// The deferred request coalesces into exactly one release at the next
/// drain, and a drain without a request is inert. Serialized: the request
/// flag is process-wide.
#[test]
#[serial_test::serial(MEMORY_RELEASE_DEFER)]
fn deferred_request_coalesces_and_drains_once() {
test_support::install_counting_hook();
run_deferred_release(); // drain any stale request
let before = test_support::calls();
run_deferred_release();
assert_eq!(
test_support::calls(),
before,
"drain without request is inert"
);
request_release_after_draw();
request_release_after_draw(); // coalesces
assert_eq!(
test_support::calls(),
before,
"requesting must not purge synchronously"
);
run_deferred_release();
assert_eq!(test_support::calls(), before + 1, "one drain, one purge");
run_deferred_release();
assert_eq!(
test_support::calls(),
before + 1,
"flag cleared by the drain"
);
}
}