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,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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user