Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
231 lines
9.5 KiB
Rust
231 lines
9.5 KiB
Rust
//! Strongly-typed notification metadata.
|
|
//!
|
|
//! Parses the `_meta` JSON from `SessionNotification` into typed fields that
|
|
//! gracefully degrade when kigi-shell hasn't been updated or meta is absent.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Parsed fields from `SessionNotification._meta`.
|
|
///
|
|
/// Extracted once in [`acp_handler`](super::super::app::acp_handler) and passed
|
|
/// downstream to the tracker and agent state.
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct NotificationMeta {
|
|
/// Accumulated token count across the session (`totalTokens`).
|
|
pub total_tokens: Option<u64>,
|
|
/// UTC ms when this notification was sent (`agentTimestampMs`).
|
|
pub agent_timestamp_ms: Option<i64>,
|
|
/// UTC ms when the current LLM streaming response started (`streamStartMs`).
|
|
/// Resets each tool-use loop iteration.
|
|
pub stream_start_ms: Option<i64>,
|
|
/// UTC ms when the current turn started (`turnStartMs`).
|
|
/// Constant for the entire turn.
|
|
pub turn_start_ms: Option<i64>,
|
|
/// Stable id for the prompt this notification belongs to (`promptId`).
|
|
/// The client passes a UUID in `PromptRequest._meta.promptId`; the agent
|
|
/// echoes it on every notification it emits while processing that
|
|
/// prompt. Used to drop chunks for cancelled / rewound turns.
|
|
pub prompt_id: Option<String>,
|
|
/// Whether this notification is historical replay from `session/load`.
|
|
pub is_replay: bool,
|
|
/// Raw `eventId` string (`"{sessionId}-{counter}"`). Tracked per session
|
|
/// as the reconnect cursor (`_meta.cursor` on `session/load`): the agent
|
|
/// resolves it by exact string match against persisted lines, so the full
|
|
/// id is kept — the numeric suffix alone is ambiguous across the
|
|
/// non-monotonic counter runs of a multi-resume history.
|
|
pub event_id: Option<String>,
|
|
/// Monotonic per-process sequence parsed from `eventId`
|
|
/// (`"{sessionId}-{counter}"`, see `kigi-shell util::event_id`). The
|
|
/// agent stamps the SAME `eventId` on the live emission and on the persisted
|
|
/// line that is later replayed, so a client can dedup an event it receives
|
|
/// twice (replay/live overlap, a re-emit after the reconnect gate, or
|
|
/// duplicate routing). Per-session events arrive in increasing order, so the
|
|
/// pager keeps a highwater and drops anything `<=` it. `None` when the agent
|
|
/// didn't stamp an `eventId` (older shell) — such updates always apply.
|
|
pub event_seq: Option<u64>,
|
|
}
|
|
|
|
/// Serializable counterpart of the replay stamp the agent injects on
|
|
/// replayed notifications (`_meta.isReplay`, stamped by kigi-shell's
|
|
/// `forward_raw_replay_line` during `session/load`).
|
|
///
|
|
/// [`NotificationMeta::from_json`] is the parse side; this is the build
|
|
/// side, so code that constructs a replay-stamped `_meta` (test fixtures,
|
|
/// playgrounds) shares the wire key with the parser instead of hand-writing
|
|
/// `json!` literals.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReplayMetaStamp {
|
|
pub is_replay: bool,
|
|
}
|
|
|
|
impl ReplayMetaStamp {
|
|
/// `_meta` value for a replayed (`session/load`) notification.
|
|
pub fn replayed() -> serde_json::Value {
|
|
serde_json::to_value(Self { is_replay: true }).expect("serialize replay meta stamp")
|
|
}
|
|
}
|
|
|
|
/// User-prompt content-block `_meta` keys (`TextContent.meta`), shared by the
|
|
/// producers (`dispatch/queue.rs` drain, `effects.rs` prompt send) and the
|
|
/// replay consumer (`acp/tracker.rs` `handle_user_message`) so the wire keys
|
|
/// cannot drift. Tests keep raw literals — they pin the wire values.
|
|
pub mod user_prompt_meta {
|
|
/// Clean display text shown in scrollback instead of the wire text.
|
|
pub const DISPLAY_TEXT: &str = "displayText";
|
|
/// Render the display text as a skill invocation (teal leading token).
|
|
pub const DISPLAY_AS_SKILL: &str = "displayAsSkill";
|
|
/// Render the display text as a scheduled (cron) prompt.
|
|
pub const DISPLAY_AS_CRON: &str = "displayAsCron";
|
|
/// `[[start, end], …]` byte ranges of recognized slash tokens into the
|
|
/// block's `text`; only meaningful when that text is displayed verbatim
|
|
/// (never stamped alongside `displayText`).
|
|
pub const SKILL_TOKEN_RANGES: &str = "skillTokenRanges";
|
|
}
|
|
|
|
/// `UserMessageChunk` / `ContentChunk._meta` keys stamped by the shell and
|
|
/// read by the pager (live and on replay).
|
|
pub mod user_message_chunk_meta {
|
|
/// Prompt index for rewind / attribution.
|
|
pub const PROMPT_INDEX: &str = "promptIndex";
|
|
/// When true, the chunk must not become a scrollback user prompt
|
|
/// ([`kigi_shell::session::PromptOrigin::hide_user_echo_from_scrollback`]).
|
|
pub const HIDE_FROM_SCROLLBACK: &str = "hideFromScrollback";
|
|
}
|
|
|
|
impl NotificationMeta {
|
|
/// Parse from the `_meta` JSON map on a `SessionNotification`.
|
|
pub fn from_json(meta: Option<&serde_json::Map<String, serde_json::Value>>) -> Self {
|
|
let Some(m) = meta else {
|
|
return Self::default();
|
|
};
|
|
let event_id = m
|
|
.get("eventId")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
// `eventId` is `"{sessionId}-{counter}"`; the counter is the part
|
|
// after the LAST '-' (session ids themselves contain '-').
|
|
let event_seq = event_id
|
|
.as_deref()
|
|
.and_then(|s| s.rsplit('-').next())
|
|
.and_then(|c| c.parse::<u64>().ok());
|
|
Self {
|
|
total_tokens: m.get("totalTokens").and_then(|v| v.as_u64()),
|
|
agent_timestamp_ms: m.get("agentTimestampMs").and_then(|v| v.as_i64()),
|
|
stream_start_ms: m.get("streamStartMs").and_then(|v| v.as_i64()),
|
|
turn_start_ms: m.get("turnStartMs").and_then(|v| v.as_i64()),
|
|
prompt_id: m
|
|
.get("promptId")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string()),
|
|
is_replay: m.get("isReplay").and_then(|v| v.as_bool()).unwrap_or(false),
|
|
event_id,
|
|
event_seq,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn parse_full_meta() {
|
|
let meta_json = json!({
|
|
"totalTokens": 5000u64,
|
|
"agentTimestampMs": 1700000000000i64,
|
|
"streamStartMs": 1700000000000i64 - 3200,
|
|
"turnStartMs": 1700000000000i64 - 5000,
|
|
"eventId": "sess-1-7",
|
|
});
|
|
let map = meta_json.as_object().unwrap();
|
|
let meta = NotificationMeta::from_json(Some(map));
|
|
|
|
assert_eq!(meta.total_tokens, Some(5000));
|
|
assert_eq!(meta.agent_timestamp_ms, Some(1700000000000));
|
|
assert_eq!(meta.stream_start_ms, Some(1700000000000 - 3200));
|
|
assert_eq!(meta.turn_start_ms, Some(1700000000000 - 5000));
|
|
assert!(!meta.is_replay);
|
|
assert_eq!(meta.event_id.as_deref(), Some("sess-1-7"));
|
|
assert_eq!(meta.event_seq, Some(7));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_missing_new_fields() {
|
|
// Simulate old kigi-shell that doesn't send streamStartMs/turnStartMs
|
|
let meta_json = json!({
|
|
"totalTokens": 1000u64,
|
|
"agentTimestampMs": 1700000000000i64,
|
|
});
|
|
let map = meta_json.as_object().unwrap();
|
|
let meta = NotificationMeta::from_json(Some(map));
|
|
|
|
assert_eq!(meta.total_tokens, Some(1000));
|
|
assert_eq!(meta.agent_timestamp_ms, Some(1700000000000));
|
|
assert_eq!(meta.stream_start_ms, None);
|
|
assert_eq!(meta.turn_start_ms, None);
|
|
assert!(!meta.is_replay);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_replay_flag() {
|
|
let meta_json = json!({
|
|
"isReplay": true,
|
|
});
|
|
let map = meta_json.as_object().unwrap();
|
|
let meta = NotificationMeta::from_json(Some(map));
|
|
|
|
assert!(meta.is_replay);
|
|
}
|
|
|
|
/// The build side ([`ReplayMetaStamp::replayed`]) and the parse side
|
|
/// ([`NotificationMeta::from_json`]) must agree on the wire key — a
|
|
/// rename on either side breaks replay detection silently otherwise.
|
|
#[test]
|
|
fn replay_meta_stamp_round_trips_through_parser() {
|
|
let stamp = ReplayMetaStamp::replayed();
|
|
let map = stamp.as_object().unwrap();
|
|
let meta = NotificationMeta::from_json(Some(map));
|
|
assert!(meta.is_replay, "stamped meta must parse as a replay");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_event_id_keeps_raw_string_and_seq() {
|
|
let meta_json = json!({
|
|
"eventId": "sess-ab-12-42",
|
|
});
|
|
let map = meta_json.as_object().unwrap();
|
|
let meta = NotificationMeta::from_json(Some(map));
|
|
|
|
assert_eq!(meta.event_id.as_deref(), Some("sess-ab-12-42"));
|
|
assert_eq!(meta.event_seq, Some(42));
|
|
}
|
|
|
|
/// A non-numeric suffix yields no seq (dedup disabled) but the raw id is
|
|
/// still kept for the reconnect cursor (string-matched agent-side).
|
|
#[test]
|
|
fn parse_event_id_without_numeric_suffix() {
|
|
let meta_json = json!({
|
|
"eventId": "weird-id-zzz",
|
|
});
|
|
let map = meta_json.as_object().unwrap();
|
|
let meta = NotificationMeta::from_json(Some(map));
|
|
|
|
assert_eq!(meta.event_id.as_deref(), Some("weird-id-zzz"));
|
|
assert_eq!(meta.event_seq, None);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_none_meta() {
|
|
let meta = NotificationMeta::from_json(None);
|
|
assert_eq!(meta.total_tokens, None);
|
|
assert_eq!(meta.agent_timestamp_ms, None);
|
|
assert_eq!(meta.stream_start_ms, None);
|
|
assert_eq!(meta.turn_start_ms, None);
|
|
assert!(!meta.is_replay);
|
|
assert_eq!(meta.event_id, None);
|
|
assert_eq!(meta.event_seq, None);
|
|
}
|
|
}
|