diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index efe23dd..8af12a8 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -88,3 +88,4 @@ {"id":"int-96f16ee3","kind":"field_change","created_at":"2026-06-30T22:27:11.180142339Z","actor":"dawn","issue_id":"klbr-rds","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Split klbr-bench main.rs into cli, continuous_loop, and longmem modules while preserving bench behavior"}} {"id":"int-5c780dfb","kind":"field_change","created_at":"2026-06-30T22:34:06.198966004Z","actor":"dawn","issue_id":"klbr-f8w","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Split Discord formatting, inbox storage, and tool schema builders into modules and deduplicated batch row rendering prep"}} {"id":"int-1f1c3a07","kind":"field_change","created_at":"2026-07-01T08:52:06.275727261Z","actor":"dawn","issue_id":"klbr-5h3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Re-exported core DTO types from klbr-ipc and removed daemon field-by-field mappers"}} +{"id":"int-623e8776","kind":"field_change","created_at":"2026-07-01T08:54:40.646821049Z","actor":"dawn","issue_id":"klbr-2kc","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Collapsed KDL optional-node parser helpers and centralized serde integer deserializers"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 7fe38d1..9076b99 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -48,7 +48,7 @@ {"_type":"issue","id":"klbr-f8w","title":"discord: decompose monolithic lib.rs and deduplicate batch formatting","description":"klbr-discord lib.rs is a 2150-line monolith containing gateway loop, state database, formatting styles, and tools. Split into logical submodules, and deduplicate bracket/xml string formatting loops.","status":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-30T21:40:14Z","created_by":"dawn","updated_at":"2026-06-30T22:34:06Z","started_at":"2026-06-30T22:27:44Z","closed_at":"2026-06-30T22:34:06Z","close_reason":"Split Discord formatting, inbox storage, and tool schema builders into modules and deduplicated batch row rendering prep","dependency_count":0,"dependent_count":0,"comment_count":0} {"_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":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-30T21:40:08Z","created_by":"dawn","updated_at":"2026-06-30T22:04:39Z","started_at":"2026-06-30T22:03:35Z","closed_at":"2026-06-30T22:04:39Z","close_reason":"Changed daemon default websocket bind from 0.0.0.0:8765 to 127.0.0.1:8765 and replaced DumpMemories std::fs::write with tokio::fs::write(...).await. Verified rg for old patterns, cargo fmt --check, and cargo test -p klbr-daemon.","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":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-30T21:40:01Z","created_by":"dawn","updated_at":"2026-07-01T08:52:06Z","started_at":"2026-06-30T22:34:30Z","closed_at":"2026-07-01T08:52:06Z","close_reason":"Re-exported core DTO types from klbr-ipc and removed daemon field-by-field mappers","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-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":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-30T21:39:55Z","created_by":"dawn","updated_at":"2026-07-01T08:54:41Z","started_at":"2026-07-01T08:52:25Z","closed_at":"2026-07-01T08:54:41Z","close_reason":"Collapsed KDL optional-node parser helpers and centralized serde integer deserializers","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} diff --git a/klbr-core/src/config/parser.rs b/klbr-core/src/config/parser.rs index 5cef296..1c27dc2 100644 --- a/klbr-core/src/config/parser.rs +++ b/klbr-core/src/config/parser.rs @@ -1,10 +1,11 @@ use anyhow::{anyhow, bail, Result}; use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue}; -use serde::{de::Error as _, Deserialize, Deserializer}; use super::{Config, MemoryConfig}; use crate::models::{ConfigValue, ModelConfig, ModelsConfig}; +pub(super) use crate::serde_utils::{deserialize_u64, deserialize_usize}; + #[derive(Clone, Debug, Default)] struct ModelRefConfig { profile: Option, @@ -56,22 +57,6 @@ impl ModelRefConfig { } } -pub(super) fn deserialize_usize<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let value = i64::deserialize(deserializer)?; - usize::try_from(value).map_err(|_| D::Error::custom("expected a non-negative usize")) -} - -pub(super) fn deserialize_u64<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let value = i64::deserialize(deserializer)?; - u64::try_from(value).map_err(|_| D::Error::custom("expected a non-negative u64")) -} - pub(super) fn parse_kdl_config(contents: &str) -> Result { let doc = contents.parse::()?; parse_kdl_document(&doc) @@ -253,52 +238,31 @@ fn ensure_known_model_fields(node: &KdlNode, allow_name: bool, allow_profile: bo Ok(()) } -fn optional_string_node(doc: &KdlDocument, name: &str) -> Result> { - doc.get(name).map(parse_string_scalar_node).transpose() -} - -fn optional_optional_string_node(doc: &KdlDocument, name: &str) -> Result>> { - doc.get(name) - .map(parse_optional_string_scalar_node) - .transpose() -} - -fn optional_bool_node(doc: &KdlDocument, name: &str) -> Result> { - doc.get(name).map(parse_bool_scalar_node).transpose() -} - -fn optional_usize_node(doc: &KdlDocument, name: &str) -> Result> { - doc.get(name).map(parse_usize_scalar_node).transpose() -} - -fn optional_u64_node(doc: &KdlDocument, name: &str) -> Result> { - doc.get(name).map(parse_u64_scalar_node).transpose() -} - -fn optional_f32_node(doc: &KdlDocument, name: &str) -> Result> { - doc.get(name).map(parse_f32_scalar_node).transpose() -} - -fn optional_optional_f32_node(doc: &KdlDocument, name: &str) -> Result>> { - doc.get(name) - .map(parse_optional_f32_scalar_node) - .transpose() +fn optional_node( + doc: &KdlDocument, + name: &str, + parse: fn(&KdlNode) -> Result, +) -> Result> { + doc.get(name).map(parse).transpose() } -fn optional_optional_u32_node(doc: &KdlDocument, name: &str) -> Result>> { - doc.get(name) - .map(parse_optional_u32_scalar_node) - .transpose() +macro_rules! optional_node_parser { + ($name:ident -> $ty:ty, $parse:ident) => { + fn $name(doc: &KdlDocument, name: &str) -> Result> { + optional_node(doc, name, $parse) + } + }; } -fn optional_optional_u32_list_node( - doc: &KdlDocument, - name: &str, -) -> Result>>> { - doc.get(name) - .map(parse_optional_u32_list_node_value) - .transpose() -} +optional_node_parser!(optional_string_node -> String, parse_string_scalar_node); +optional_node_parser!(optional_optional_string_node -> Option, parse_optional_string_scalar_node); +optional_node_parser!(optional_bool_node -> bool, parse_bool_scalar_node); +optional_node_parser!(optional_usize_node -> usize, parse_usize_scalar_node); +optional_node_parser!(optional_u64_node -> u64, parse_u64_scalar_node); +optional_node_parser!(optional_f32_node -> f32, parse_f32_scalar_node); +optional_node_parser!(optional_optional_f32_node -> Option, parse_optional_f32_scalar_node); +optional_node_parser!(optional_optional_u32_node -> Option, parse_optional_u32_scalar_node); +optional_node_parser!(optional_optional_u32_list_node -> Vec>, parse_optional_u32_list_node_value); fn parse_string_scalar_node(node: &KdlNode) -> Result { match node_to_config_value(node)? { diff --git a/klbr-core/src/lib.rs b/klbr-core/src/lib.rs index ea9a2e3..df778a1 100644 --- a/klbr-core/src/lib.rs +++ b/klbr-core/src/lib.rs @@ -13,6 +13,7 @@ pub mod pipeline; pub mod planner; pub mod retrieval; pub mod router; +pub(crate) mod serde_utils; pub mod support; pub mod tools; diff --git a/klbr-core/src/models.rs b/klbr-core/src/models.rs index e804248..1b24382 100644 --- a/klbr-core/src/models.rs +++ b/klbr-core/src/models.rs @@ -2,7 +2,7 @@ use anyhow::Result; use futures::StreamExt; use reqwest::{Client, RequestBuilder}; use rusqlite::OptionalExtension; -use serde::{de::Error as _, Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use std::collections::{BTreeMap, HashMap}; use std::path::Path; @@ -286,7 +286,7 @@ pub struct ModelsConfig { pub embedder: ModelConfig, pub reranker: ModelConfig, pub embedders: Vec, - #[serde(deserialize_with = "deserialize_usize")] + #[serde(deserialize_with = "crate::serde_utils::deserialize_usize")] pub embed_dim: usize, } @@ -314,14 +314,6 @@ impl Default for ModelsConfig { } } -fn deserialize_usize<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let value = i64::deserialize(deserializer)?; - usize::try_from(value).map_err(|_| D::Error::custom("expected a non-negative usize")) -} - fn build_client(config: &ModelConfig) -> Result { let accept_invalid_certs = should_accept_invalid_localhost_certs(&config.url); let mut builder = reqwest::Client::builder() diff --git a/klbr-core/src/serde_utils.rs b/klbr-core/src/serde_utils.rs new file mode 100644 index 0000000..ad7da75 --- /dev/null +++ b/klbr-core/src/serde_utils.rs @@ -0,0 +1,17 @@ +use serde::{de::Error as _, Deserialize, Deserializer}; + +pub(crate) fn deserialize_usize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value = i64::deserialize(deserializer)?; + usize::try_from(value).map_err(|_| D::Error::custom("expected a non-negative usize")) +} + +pub(crate) fn deserialize_u64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value = i64::deserialize(deserializer)?; + u64::try_from(value).map_err(|_| D::Error::custom("expected a non-negative u64")) +}