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:
@@ -0,0 +1,178 @@
|
||||
use agent_client_protocol as acp;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use crate::extensions::notification::SessionNotification as XaiSessionNotification;
|
||||
use acp::SessionNotification as AcpSessionNotification;
|
||||
|
||||
/// Notification destined for the high-frequency event ReplayBuffer).
|
||||
/// Variants tag the inner protocol surface because the
|
||||
/// merge rules and wire envelopes differ, but routing through `event_tx`
|
||||
/// and `ReplayBuffer` is by design -- anything that goes here gets
|
||||
/// debounced + merged, and emerges through `emit_buffered` without
|
||||
/// firing per-chunk hooks or persistence writes.
|
||||
///
|
||||
/// One-shot xAI events (RetryState, ImageCompressed, HookExecution,
|
||||
/// etc.) take the direct `send_xai_notification` path for per-event hooks and persistence.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum SessionNotification {
|
||||
Acp(Box<AcpSessionNotification>),
|
||||
Xai(Box<XaiSessionNotification>),
|
||||
}
|
||||
|
||||
impl SessionNotification {
|
||||
pub(crate) fn session_id(&self) -> &acp::SessionId {
|
||||
match self {
|
||||
Self::Acp(n) => &n.session_id,
|
||||
Self::Xai(n) => &n.session_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this notification is a streaming chunk that
|
||||
/// should be buffered for merging + debouncing.
|
||||
pub(crate) fn is_streaming_chunk(&self) -> bool {
|
||||
match self {
|
||||
Self::Acp(n) => matches!(
|
||||
n.update,
|
||||
acp::SessionUpdate::AgentMessageChunk(_) | acp::SessionUpdate::AgentThoughtChunk(_)
|
||||
),
|
||||
Self::Xai(n) => matches!(
|
||||
n.update,
|
||||
crate::extensions::notification::SessionUpdate::ToolCallDeltaChunk { .. }
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract `agentTimestampMs` from the notification's meta, if set.
|
||||
pub(crate) fn agent_timestamp_ms(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Acp(n) => n
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("agentTimestampMs"))
|
||||
.and_then(|v| v.as_u64()),
|
||||
Self::Xai(n) => n
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("agentTimestampMs"))
|
||||
.and_then(|v| v.as_u64()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this notification can be merged with `prev`'s pending slot based on their timestamps.
|
||||
pub(crate) fn is_in_timestamp_window(&self, prev: &Self, max_duration_ms: u64) -> bool {
|
||||
match (prev.agent_timestamp_ms(), self.agent_timestamp_ms()) {
|
||||
// ACP events have timestamps, so we can window-check.
|
||||
(Some(prev_ts), Some(incoming_ts)) => incoming_ts <= prev_ts + max_duration_ms,
|
||||
// Either side missing the agentTimestampMs meta means we can't window-check.
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AcpSessionNotification> for SessionNotification {
|
||||
fn from(n: AcpSessionNotification) -> Self {
|
||||
Self::Acp(Box::new(n))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<XaiSessionNotification> for SessionNotification {
|
||||
fn from(n: XaiSessionNotification) -> Self {
|
||||
Self::Xai(Box::new(n))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SessionNotification {
|
||||
/// Test-only: borrow the inner ACP notification, panicking if this
|
||||
/// is not the `Acp` variant.
|
||||
pub(crate) fn expect_acp(&self) -> &AcpSessionNotification {
|
||||
match self {
|
||||
Self::Acp(n) => n,
|
||||
Self::Xai(_) => panic!("expected Acp notification, got Xai"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-only: move the inner ACP notification out, panicking if
|
||||
/// this is not the `Acp` variant.
|
||||
pub(crate) fn into_acp(self) -> AcpSessionNotification {
|
||||
match self {
|
||||
Self::Acp(n) => *n,
|
||||
Self::Xai(_) => panic!("expected Acp notification, got Xai"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SessionEvent {
|
||||
Notification(SessionNotification),
|
||||
FlushReplay {
|
||||
respond_to: Option<oneshot::Sender<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl SessionEvent {
|
||||
pub(crate) fn flush_with_ack() -> (Self, oneshot::Receiver<()>) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
(
|
||||
Self::FlushReplay {
|
||||
respond_to: Some(tx),
|
||||
},
|
||||
rx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum FlushReplayError {
|
||||
EventChannelClosed,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Flush replay-buffered notifications through the session actor loop.
|
||||
///
|
||||
/// This must only be used from callers that are *outside* `run_session()`.
|
||||
/// The `FlushComplete` command runs inside the actor loop and therefore
|
||||
/// flushes `replay_buffer` inline to avoid waiting on a mailbox event that
|
||||
/// the same loop would need to process.
|
||||
pub(crate) async fn flush_replay_actor(
|
||||
event_tx: &mpsc::UnboundedSender<SessionEvent>,
|
||||
) -> Result<(), FlushReplayError> {
|
||||
let (event, rx) = SessionEvent::flush_with_ack();
|
||||
event_tx
|
||||
.send(event)
|
||||
.map_err(|_| FlushReplayError::EventChannelClosed)?;
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), rx)
|
||||
.await
|
||||
.map_err(|_| FlushReplayError::Timeout)?
|
||||
.map_err(|_| FlushReplayError::EventChannelClosed)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn flush_replay_actor_acknowledges() {
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local
|
||||
.run_until(async {
|
||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel::<SessionEvent>();
|
||||
let (event, rx) = SessionEvent::flush_with_ack();
|
||||
event_tx.send(event).expect("event send should succeed");
|
||||
|
||||
let event = event_rx.recv().await.expect("event should arrive");
|
||||
|
||||
match event {
|
||||
SessionEvent::FlushReplay { respond_to } => {
|
||||
let tx = respond_to.expect("flush replay should carry ack sender");
|
||||
tx.send(()).expect("ack send should succeed");
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
|
||||
rx.await.expect("ack should be received");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user