diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -81,3 +81,4 @@ {"id":"int-5d3aa77d","kind":"field_change","created_at":"2026-06-30T21:57:30.338732657Z","actor":"dawn","issue_id":"klbr-4ls","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented time-aware session-first ranking by threading ref event timestamps into EvidenceAtom and applying a bounded reference_time temporal score for episodic/live candidates, with canonical ref_timestamp preferred over promptable computed_at and sentinel/future safeguards. Stage-one traces now expose timestamp and temporal_score. Verified with klbr-core tests (166 passed, 1 ignored), klbr-bench tests (19 passed), fmt/diff checks, and retrieval-only LongMemEval-S smokes: sample20 at /tmp/klbr-time-aware-sample20 and corrected sample5 at /tmp/klbr-time-aware-sample5-v2."}} {"id":"int-72e1b1bb","kind":"field_change","created_at":"2026-06-30T21:59:42.144961313Z","actor":"dawn","issue_id":"klbr-u03","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Replaced hardcoded /home/mayer paths in antigravity bridge auth with dirs::home_dir()-based helpers, added ANTIGRAVITY_BRIDGE_CONFIG_DIR for token/cache files, and made the agy binary path configurable via ANTIGRAVITY_BRIDGE_BINARY_PATH or AGY_BINARY_PATH with ~/.local/bin/agy fallback. Verified cargo fmt --check and cargo test -p antigravity-bridge (9 passed)."}} {"id":"int-9f29f590","kind":"field_change","created_at":"2026-06-30T22:01:41.091400446Z","actor":"dawn","issue_id":"klbr-noi","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Replaced the missing ./default.nix package with config.nci.outputs.\"klbr-daemon\".packages.release. Verified nix eval .#packages.x86_64-linux.default.name returns klbr-daemon and nix flake show succeeds without the default.nix error."}} +{"id":"int-686e2908","kind":"field_change","created_at":"2026-06-30T22:03:16.088748369Z","actor":"dawn","issue_id":"klbr-zue","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Replaced shell stdout/stderr byte slicing with char-boundary truncation helper and added UTF-8 regression tests. Verified cargo test -p klbr-core shell_truncation, full cargo test -p klbr-core (168 passed, 1 ignored), cargo fmt --check, git diff --check, and rg confirms the unsafe stdout/stderr slices are gone."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -49,7 +49,7 @@ {"_type":"issue","id":"klbr-41z","title":"daemon: fix blocking std::fs calls and secure default WS bind address","description":"DumpMemories handler in daemon.rs blocks the tokio runtime thread using sync std::fs::write. Fix to tokio::fs::write. Also, change default WS bind address to 127.0.0.1 for local-only safety.","status":"open","priority":2,"issue_type":"task","owner":"90008@klbr.net","created_at":"2026-06-30T21:40:08Z","created_by":"dawn","updated_at":"2026-06-30T21:40:08Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"klbr-5h3","title":"ipc/daemon: eliminate duplicate DTO structs and field mapping boilerplate","description":"HistoryEntry, ToolCall, CompactionRecord, and ResolutionEventDto are identical duplicates between klbr-ipc and klbr-core. The daemon has extensive boilerplate mapping them field-by-field. Share or re-export these types.","status":"open","priority":2,"issue_type":"task","owner":"90008@klbr.net","created_at":"2026-06-30T21:40:01Z","created_by":"dawn","updated_at":"2026-06-30T21:40:01Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"klbr-2kc","title":"core: reduce KDL config parser boilerplate in parser.rs","description":"parser.rs contains ~20 copy-pasted optional_*_node helpers that share identical structure. Simplify with macros or generic helpers. Also resolve duplicate deserialize_usize.","status":"open","priority":2,"issue_type":"task","owner":"90008@klbr.net","created_at":"2026-06-30T21:39:55Z","created_by":"dawn","updated_at":"2026-06-30T21:39:55Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"klbr-zue","title":"core: fix shell.rs unsafe byte-slice truncation","description":"shell.rs truncates command output using byte offsets on a lossy UTF-8 string (e.g. stdout[..20_000]), which can panic if slicing in the middle of a multi-byte character. Truncate by char count or indices instead.","status":"open","priority":2,"issue_type":"task","owner":"90008@klbr.net","created_at":"2026-06-30T21:39:48Z","created_by":"dawn","updated_at":"2026-06-30T21:39:48Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"klbr-zue","title":"core: fix shell.rs unsafe byte-slice truncation","description":"shell.rs truncates command output using byte offsets on a lossy UTF-8 string (e.g. stdout[..20_000]), which can panic if slicing in the middle of a multi-byte character. Truncate by char count or indices instead.","status":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-30T21:39:48Z","created_by":"dawn","updated_at":"2026-06-30T22:03:16Z","started_at":"2026-06-30T22:02:13Z","closed_at":"2026-06-30T22:03:16Z","close_reason":"Replaced shell stdout/stderr byte slicing with char-boundary truncation helper and added UTF-8 regression tests. Verified cargo test -p klbr-core shell_truncation, full cargo test -p klbr-core (168 passed, 1 ignored), cargo fmt --check, git diff --check, and rg confirms the unsafe stdout/stderr slices are gone.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"klbr-3ha","title":"core: refactor models.rs media stripping and SSE parser duplication","description":"strip_media_urls and replace_media_urls_with_placeholders share near-identical regex parsing logic. Also, complete() and stream() duplicate manual SSE chunk parsing. Clean these up.","status":"open","priority":2,"issue_type":"task","owner":"90008@klbr.net","created_at":"2026-06-30T21:39:42Z","created_by":"dawn","updated_at":"2026-06-30T21:39:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"klbr-647","title":"core: migrate Message.role from String to Role enum","description":"Message.role is currently represented as a raw String, which leads to fragile string comparisons ('assistant', 'tool', etc.) all over the codebase. Migrate to a proper Role enum.","status":"open","priority":2,"issue_type":"task","owner":"90008@klbr.net","created_at":"2026-06-30T21:39:36Z","created_by":"dawn","updated_at":"2026-06-30T21:39:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"klbr-so2","title":"core: decompose agent.rs run_turn() god method","description":"agent.rs run_turn() is 500+ lines, managing execution, tool loops, preemption, and discord integration. Break down tool execution loops and phase handlers into separate functions.","status":"open","priority":2,"issue_type":"task","owner":"90008@klbr.net","created_at":"2026-06-30T21:39:29Z","created_by":"dawn","updated_at":"2026-06-30T21:39:29Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/klbr-core/src/tools/shell.rs b/klbr-core/src/tools/shell.rs --- a/klbr-core/src/tools/shell.rs +++ b/klbr-core/src/tools/shell.rs @@ -53,24 +53,46 @@ let mut result = format!("exit {code}"); if !stdout.is_empty() { result.push('\n'); - if stdout.len() > 20_000 { - result.push_str(&stdout[..20_000]); - result.push_str("\n[...truncated]"); - } else { - result.push_str(&stdout); - } + push_truncated_chars(&mut result, &stdout, 20_000); } if !stderr.is_empty() { result.push_str("\nstderr:\n"); - if stderr.len() > 5_000 { - result.push_str(&stderr[..5_000]); - result.push_str("\n[...truncated]"); - } else { - result.push_str(&stderr); - } + push_truncated_chars(&mut result, &stderr, 5_000); } result } Err(e) => format!("error: {e}"), + } +} + +fn push_truncated_chars(out: &mut String, value: &str, max_chars: usize) { + let mut chars = value.chars(); + let truncated = chars.by_ref().take(max_chars).collect::(); + out.push_str(&truncated); + if chars.next().is_some() { + out.push_str("\n[...truncated]"); + } +} + +#[cfg(test)] +mod tests { + use super::push_truncated_chars; + + #[test] + fn shell_truncation_respects_utf8_boundaries() { + let mut out = String::new(); + + push_truncated_chars(&mut out, "éé", 1); + + assert_eq!(out, "é\n[...truncated]"); + } + + #[test] + fn shell_truncation_does_not_mark_exact_length_output() { + let mut out = String::new(); + + push_truncated_chars(&mut out, "abc", 3); + + assert_eq!(out, "abc"); } }