Skip to main content

agent_doc_element/
id.rs

1//! Boundary ID helpers for `agent:boundary` markers.
2//!
3//! - [`new_boundary_id`] — 8-hex-char UUID v4 prefix (length controlled by
4//!   [`BOUNDARY_ID_LEN`]).
5//! - [`new_boundary_id_with_summary`] — same ID optionally suffixed with a
6//!   3-word, 20-char-max slug derived from `summary`
7//!   (format: `a0cfeb34:boundary-fix`).
8//! - [`format_boundary_marker`] — renders the canonical HTML comment form
9//!   `<!-- agent:boundary:<id> -->` used in document component boundaries.
10//!
11//! Non-deterministic: `new_boundary_id` uses UUID v4 — callers must not rely
12//! on ordering.
13
14/// Default number of hex characters for boundary IDs.
15pub const BOUNDARY_ID_LEN: usize = 8;
16
17/// Generate a new boundary ID (short hex string from UUID v4).
18pub fn new_boundary_id() -> String {
19    let full = uuid::Uuid::new_v4().to_string().replace('-', "");
20    full[..BOUNDARY_ID_LEN.min(full.len())].to_string()
21}
22
23/// Generate a boundary ID with an optional summary suffix.
24///
25/// Format: `a0cfeb34` or `a0cfeb34:boundary-fix` (with summary).
26/// The summary is slugified (lowercase, non-alphanumeric → `-`, max 20 chars).
27pub fn new_boundary_id_with_summary(summary: Option<&str>) -> String {
28    with_summary_slug(new_boundary_id(), summary)
29}
30
31/// Derive a *deterministic* boundary ID from a stable seed (for example the IPC
32/// `patch_id`), optionally suffixed with a summary slug. Same seed → same ID.
33///
34/// This is the key to `#finalize-visible-buffer-ipc-timeout-race` corruption:
35/// a single logical write can build its IPC patches more than once (socket
36/// attempt, then file-IPC fallback, then the `run_stream` timeout re-write).
37/// When each build minted a fresh random boundary, the plugin received the same
38/// response under different boundary IDs and, unable to match an already-applied
39/// boundary, appended it a second time — doubling the editor buffer. Seeding the
40/// boundary from the stable `patch_id` makes every rebuild carry an identical
41/// boundary the plugin can dedup/replace instead of appending.
42pub fn boundary_id_from_seed_with_summary(seed: &str, summary: Option<&str>) -> String {
43    let hex: String = seed
44        .chars()
45        .filter(|c| c.is_ascii_hexdigit())
46        .map(|c| c.to_ascii_lowercase())
47        .collect();
48    let id = if hex.len() >= BOUNDARY_ID_LEN {
49        hex[..BOUNDARY_ID_LEN].to_string()
50    } else {
51        // Seed without enough hex digits (shouldn't happen for a UUID patch_id):
52        // fold to a stable 8-hex value so the result is still deterministic.
53        let folded = seed
54            .bytes()
55            .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
56        format!("{folded:08x}")
57    };
58    with_summary_slug(id, summary)
59}
60
61/// Append a slugified summary suffix to a boundary ID, if `summary` is non-empty.
62fn with_summary_slug(id: String, summary: Option<&str>) -> String {
63    match summary {
64        Some(s) if !s.is_empty() => {
65            let slug: String = s
66                .to_lowercase()
67                .chars()
68                .map(|c| if c.is_alphanumeric() { c } else { '-' })
69                .collect::<String>()
70                .split('-')
71                .filter(|s| !s.is_empty())
72                .take(3) // max 3 words
73                .collect::<Vec<&str>>()
74                .join("-");
75            let slug = &slug[..slug.len().min(20)];
76            format!("{}:{}", id, slug)
77        }
78        _ => id,
79    }
80}
81
82/// Format a boundary marker comment.
83pub fn format_boundary_marker(id: &str) -> String {
84    format!("<!-- agent:boundary:{} -->", id)
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn new_boundary_id_len() {
93        assert_eq!(new_boundary_id().len(), BOUNDARY_ID_LEN);
94        assert!(new_boundary_id().chars().all(|c| c.is_ascii_hexdigit()));
95    }
96
97    #[test]
98    fn new_boundary_id_with_summary_none() {
99        assert_eq!(new_boundary_id_with_summary(None).len(), BOUNDARY_ID_LEN);
100    }
101
102    #[test]
103    fn new_boundary_id_with_summary_empty() {
104        assert_eq!(
105            new_boundary_id_with_summary(Some("")).len(),
106            BOUNDARY_ID_LEN
107        );
108    }
109
110    #[test]
111    fn new_boundary_id_with_summary_slug() {
112        let id = new_boundary_id_with_summary(Some("Boundary Fix"));
113        assert!(id.ends_with(":boundary-fix"), "got: {}", id);
114    }
115
116    #[test]
117    fn new_boundary_id_with_summary_truncate() {
118        let long = "this is a very long summary that should be truncated and clipped";
119        let id = new_boundary_id_with_summary(Some(long));
120        let (_, slug) = id.split_once(':').unwrap();
121        assert!(slug.len() <= 20, "slug too long: {}", slug);
122    }
123
124    #[test]
125    fn format_boundary_marker_renders_correctly() {
126        assert_eq!(
127            format_boundary_marker("abc123"),
128            "<!-- agent:boundary:abc123 -->"
129        );
130    }
131
132    #[test]
133    fn boundary_id_from_seed_is_deterministic() {
134        // #finalize-visible-buffer-ipc-timeout-race: the same patch_id seed must
135        // always derive the same boundary so socket/file/fallback rebuilds carry
136        // an identical boundary the plugin dedups instead of appending.
137        let patch_id = "2ffa57c0-24e8-441c-aca9-46e6aa6f1c2a";
138        let a = boundary_id_from_seed_with_summary(patch_id, None);
139        let b = boundary_id_from_seed_with_summary(patch_id, None);
140        assert_eq!(a, b, "same seed must yield the same boundary id");
141        assert_eq!(a.len(), BOUNDARY_ID_LEN);
142        assert_eq!(a, "2ffa57c0", "boundary derives from the seed's hex prefix");
143        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
144    }
145
146    #[test]
147    fn boundary_id_from_seed_differs_across_seeds() {
148        let a = boundary_id_from_seed_with_summary("2ffa57c0-24e8-441c-aca9-46e6aa6f1c2a", None);
149        let b = boundary_id_from_seed_with_summary("99887766-1111-2222-3333-444455556666", None);
150        assert_ne!(a, b, "different writes must not collide on one boundary");
151    }
152
153    #[test]
154    fn boundary_id_from_seed_keeps_summary_slug() {
155        let id = boundary_id_from_seed_with_summary(
156            "2ffa57c0-24e8-441c-aca9-46e6aa6f1c2a",
157            Some("Agent Doc Bugs"),
158        );
159        assert_eq!(id, "2ffa57c0:agent-doc-bugs");
160    }
161
162    #[test]
163    fn boundary_id_from_seed_without_hex_is_still_deterministic() {
164        let a = boundary_id_from_seed_with_summary("zzzz-non-hex-seed", None);
165        let b = boundary_id_from_seed_with_summary("zzzz-non-hex-seed", None);
166        assert_eq!(a, b);
167        assert_eq!(a.len(), BOUNDARY_ID_LEN);
168    }
169}