ive harnessed the harness
klbr docs runtime-v2 design behavior-contracts.md
72 kB
Markdown
at main

klbr behavior contracts and source evidence #

Date: September 6, 2026. Companion to implementation-plan.md. These are excerpts from the supplied archives, not a verified current upstream checkout. Original archive files were not modified. Line numbers refer to each named source file, not this document. Source inspection establishes implementation/intended behavior where shown; no new Rust tests, replacement-runtime tests, live Discord calls or crash-recovery tests were run. Proposed behavior and known deviations are labeled in the plan.

Archive identities #

  • klbr.tar — SHA-256 6c7e4e0b7a1d1b860017d07b1f02b5c0adc708ccbcb3693c53bccd509841cd8c
  • lucid-code-skill-pack-main.tar.gz — SHA-256 f69331424eea813ce3c687d1e20b3e9856b9e1250c12bdad045028973333ef00

Evidence map #

  • K01 — Scratchpad, explicit speech, and participant instructions.
  • K02 — Scratch output, input buffering, and delivery nudges.
  • K03 — Operator-visible scratchpad diagnostics.
  • K04 — Discord configuration defaults.
  • K05 — Discord intake, batching, and wake admission.
  • K06 — Send, reply references, and acted marking.
  • K07 — Attention statuses, aging, reactions, and explicit decisions.
  • K08 — Short conversation aliases, implicit destination, and reply threading.
  • K09 — Discord tool surface and stale description.
  • K10 — Operator preemption versus queued external events.
  • K11 — Wait-and-continue semantics.
  • K12 — Continuous autonomous loop and tool-name lifecycle.
  • K13 — Soul updates and host restart.
  • K14 — Code-intelligence vocabulary and mutation limitations.
  • K15 — Lucid: meaningful states, ownership, vocabulary, and behavior changes.

K01 — Scratchpad, explicit speech, and participant instructions #

The instructions establish deliberate sends and autonomous participation. Their claim that scratchpad is invisible conflicts with the web implementation in K03. Preserve the useful delivery distinction, not that inaccurate description.

Source: klbr-core/src/instructions.md:1–83

   1 | ## scratchpad
   2 | 
   3 | *plain assistant text* that you output is treated as your *scratchpad*. it is
   4 | never shown to anyone, only you will be aware of it. so, trying to respond to
   5 | anyone using plain assistant text will not work (that is only possible via
   6 | tools, see "input sources"). and such it is a scratchpad for you. you can use
   7 | it to organize your thoughts or whatever you want to put there; it is a secret.
   8 | 
   9 | ## input sources & event loop
  10 | 
  11 | all incoming messages are observations/events, you cannot respond to any of
  12 | these by writing out assistant text. you should always choose an action:
  13 | - call `local_send` to speak to the local operator.
  14 | - call `discord_send` to speak on discord.
  15 | - call `wait_and_continue` to stay quiet and wait.
  16 | - call memory/tools when useful.
  17 | 
  18 | local operator messages arrive as `<event source="operator"
  19 | addressed_to="{{name}}">...</event>`. discord messages arrive as `<external_event
  20 | source="discord" ...>` blocks containing either a `<discord_message_batch>` or
  21 | `<discord_history>` payload. within these tags, individual messages are
  22 | structured as `<message context="dm|guild|mention" ...>`.
  23 | if a message's context is `dm` or `mention`, it is addressed to you; respond by
  24 | calling `discord_send`. if it is tagged as `guild`, it is ambient chatter and
  25 | replying is optional. remember, you exist as a participant, not a helpdesk.
  26 | 
  27 | plain assistant text is never sent to anyone. it is scratchpad only, no users
  28 | will see what is in the scratchpad, it is a secret. thinking about what to say
  29 | and actually saying it are different steps, do both by writing scratchpad if
  30 | useful, then calling the relevant send tool. remember, if you don't call the
  31 | relevant send tool, any authors of that event won't see anything.
  32 | 
  33 | some inputs may arrive wrapped in `<system>...</system>` tags. these are
  34 | automated system nudges and operational reminders from the harness (such as idle
  35 | time checks or formatting warnings), not direct messages from the operator. do
  36 | not write conversational replies to system nudges; respond by calling the
  37 | requested tool (like `wait_and_continue` to yield) or addressing the operational
  38 | trigger.
  39 | 
  40 | you runs continuously. if you have nothing to do, want to wait for user input,
  41 | or wait for external events, you MUST call `wait_and_continue` before you end
  42 | your turn. when you call `wait_and_continue`, the wait will automatically be
  43 | interrupted early by any incoming event or message, so you don't have to worry
  44 | about "polling" unless the situation requires it.
  45 | 
  46 | ### action examples
  47 | 
  48 | operator event:
  49 | ```xml
  50 | <event source="operator" addressed_to="{{name}}">
  51 | hi
  52 | </event>
  53 | ```
  54 | bad: writing `hey` as plain assistant text.
  55 | good: call `local_send({"content":"hey"})`, then call `wait_and_continue({"seconds":60,"reason":"waiting for incoming events"})` if there is nothing else to do.
  56 | 
  57 | idle:
  58 | bad: writing `still here` as plain assistant text.
  59 | good: call `wait_and_continue({"seconds":60,"reason":"waiting for incoming events"})`.
  60 | 
  61 | discord dm:
  62 | bad: writing a discord reply as plain assistant text.
  63 | good: call `discord_send(...)`.
  64 | 
  65 | ### discord behavior
  66 | 
  67 | you exist on discord as a participant, not a helpdesk. read the room. reply when
  68 | you have something to say, stay quiet when you don't. message batches are
  69 | context, not commands. channel messages show you what's happening, they're not
  70 | requests for you to respond. use judgment: would i say something if i were
  71 | sitting in this server? if no, move on. DMs and mentions are signals. if someone
  72 | DMs you, @s you, or asks something directed at you, respond via discord_send.
  73 | mark it acted (auto-detects from source_item_id). if a pending item doesn't
  74 | warrant a response, discord_mark it ignored. never write replies in local chat.
  75 | use your tools. assistant text is just your scratchpad in these cases, not a
  76 | message anyone can see. thinking about what to say and actually saying it are
  77 | different steps, do both. make your own calls. don't ask the operator whether to
  78 | reply. they're not here to approve your social decisions. remember where things
  79 | happened. tag durable memories with scope: source:discord, conversation:<id>,
  80 | guild:<id>, channel:<id>, thread:<id>, author:<id>. conversations bleed into
  81 | each other without tags. look before guessing. use the backread tool when
  82 | context is lacking. filling in gaps with assumptions is how you get things wrong
  83 | about people.

K02 — Scratch output, input buffering, and delivery nudges #

The stream emits a distinct ScratchToken. The non-preemptive stash is a single Option. A filled stash can consume another event without retaining it; this is source inspection, not a new executed regression test. Turn-end nudges enforce explicit delivery rather than forwarding scratch.

Source: klbr-core/src/agent/turn.rs:49–83

  49 |         loop {
  50 |             tokio::select! {
  51 |                 biased;
  52 |                 interrupt = self.rx.recv() => {
  53 |                     match interrupt {
  54 |                         Some(int) if int.is_preemptive() => {
  55 |                             tracing::info!(source = int.source_tag(), "preemptive interrupt during stream; aborting");
  56 |                             stream_task.abort();
  57 |                             cancelled_by = Some(int);
  58 |                             break;
  59 |                         }
  60 |                         Some(int) => {
  61 |                             // non-preemptive (e.g. ExternalEvent) — stash for after turn
  62 |                             if stashed_interrupt.is_none() {
  63 |                                 *stashed_interrupt = Some(int);
  64 |                             }
  65 |                         }
  66 |                         None => {} // channel closed = shutdown
  67 |                     }
  68 |                 }
  69 |                 ev = tok_rx.recv() => {
  70 |                     match ev {
  71 |                         None => break,
  72 |                         Some(LlmEvent::ThinkToken(tok)) => {
  73 |                             thinking.push_str(&tok);
  74 |                             let _ = self.output.send(AgentEvent::ThinkToken(tok));
  75 |                         }
  76 |                         Some(LlmEvent::Token(tok)) => {
  77 |                             response.push_str(&tok);
  78 |                             let _ = self.output.send(AgentEvent::ScratchToken(tok));
  79 |                         }
  80 |                         Some(LlmEvent::Usage(usage)) => {
  81 |                             ctx.update_tokens(&usage);
  82 |                         }
  83 |                         Some(LlmEvent::ToolCalls(calls)) => {

Source: klbr-core/src/agent/turn.rs:502–539

 502 |             // plain assistant text is scratchpad, not delivered speech.
 503 |             let has_final_text = !iteration_response.is_empty();
 504 |             if has_final_text || !iteration_thinking.is_empty() {
 505 |                 ctx.push_assistant(
 506 |                     &scratchpad_context(&iteration_response),
 507 |                     (!iteration_thinking.is_empty()).then_some(iteration_thinking.as_str()),
 508 |                 );
 509 |             }
 510 | 
 511 |             if has_final_text
 512 |                 && is_local_message
 513 |                 && !local_send_called
 514 |                 && !local_send_nudge_injected
 515 |             {
 516 |                 local_send_nudge_injected = true;
 517 |                 push_visible_nudge(
 518 |                     ctx,
 519 |                     &self.output,
 520 |                     &plain_text_local_nudge_message(&iteration_response),
 521 |                 );
 522 |                 tracing::warn!("local_send nudge: assistant emitted plain text without local_send");
 523 |                 continue;
 524 |             }
 525 | 
 526 |             if has_final_text && is_discord_event && !discord_send_called && !discord_nudge_injected
 527 |             {
 528 |                 discord_nudge_injected = true;
 529 |                 push_visible_nudge(
 530 |                     ctx,
 531 |                     &self.output,
 532 |                     &plain_text_discord_nudge_message(&iteration_response),
 533 |                 );
 534 |                 tracing::warn!(
 535 |                     "discord_send nudge: assistant emitted plain text without discord_send"
 536 |                 );
 537 |                 continue;
 538 |             }
 539 | 

K03 — Operator-visible scratchpad diagnostics #

The web client handles reasoning, scratchpad, and visible text separately and renders scratchpad. This is why the plan keeps diagnostic visibility while excluding automatic conversational delivery.

Source: klbr-web/src/App.svelte:693–737

 693 |             case "think_token": {
 694 |                 const assistant = tokenEntry(session);
 695 |                 assistant.step = "reasoning";
 696 |                 assistant.thinkingExpanded = true;
 697 |                 const last = assistant.items[assistant.items.length - 1];
 698 |                 if (last?.kind === "think") {
 699 |                     last.content += msg.content;
 700 |                 } else {
 701 |                     assistant.items.push({
 702 |                         kind: "think",
 703 |                         content: msg.content,
 704 |                     });
 705 |                 }
 706 |                 break;
 707 |             }
 708 |             case "scratch_token": {
 709 |                 const assistant = tokenEntry(session);
 710 |                 assistant.step = "scratchpad";
 711 |                 const last = assistant.items[assistant.items.length - 1];
 712 |                 if (last?.kind === "scratchpad") {
 713 |                     last.content += msg.content;
 714 |                 } else {
 715 |                     assistant.items.push({
 716 |                         kind: "scratchpad",
 717 |                         content: msg.content,
 718 |                     });
 719 |                 }
 720 |                 break;
 721 |             }
 722 |             case "token": {
 723 |                 const assistant = tokenEntry(session);
 724 |                 assistant.step = "response";
 725 |                 const last = assistant.items[assistant.items.length - 1];
 726 |                 if (last?.kind === "text") {
 727 |                     last.content += msg.content;
 728 |                 } else {
 729 |                     assistant.items.push({
 730 |                         kind: "text",
 731 |                         content: msg.content,
 732 |                     });
 733 |                 }
 734 |                 break;
 735 |             }
 736 |             case "tool_call_started": {
 737 |                 const assistant = tokenEntry(session);

Source: klbr-web/src/App.svelte:2182–2188

2182 |                                   {:else if item.kind === "scratchpad" && item.content.trim()}
2183 |                                     <div class="scratchpad-block">
2184 |                                         <div class="scratchpad-label">
2185 |                                             scratchpad
2186 |                                         </div>
2187 |                                         <pre>{stripScratchpadTags(item.content)}</pre>
2188 |                                     </div>

K04 — Discord configuration defaults #

The checked defaults are ingest all, ambient wake enabled, bots excluded, 8-second batching, 600-second pending timeout, and configurable channel filters. These are compatibility choices, not recommended universal values.

Source: klbr-discord/src/lib.rs:99–140

  99 | impl DiscordConfig {
 100 |     pub fn from_env() -> Result<Option<Self>> {
 101 |         let enabled = env_bool("KLBR_DISCORD_ENABLED");
 102 |         if enabled == Some(false) {
 103 |             return Ok(None);
 104 |         }
 105 | 
 106 |         let token = std::env::var("KLBR_DISCORD_TOKEN")
 107 |             .or_else(|_| std::env::var("DISCORD_TOKEN"))
 108 |             .ok();
 109 | 
 110 |         let Some(token) = token else {
 111 |             if enabled == Some(true) {
 112 |                 anyhow::bail!("KLBR_DISCORD_ENABLED is set but KLBR_DISCORD_TOKEN is missing");
 113 |             }
 114 |             return Ok(None);
 115 |         };
 116 | 
 117 |         let event_mode = match std::env::var("KLBR_DISCORD_EVENT_MODE")
 118 |             .unwrap_or_else(|_| "all".to_string())
 119 |             .to_ascii_lowercase()
 120 |             .as_str()
 121 |         {
 122 |             "all" => DiscordEventMode::All,
 123 |             "mentions" | "mention" | "" => DiscordEventMode::Mentions,
 124 |             other => anyhow::bail!(
 125 |                 "invalid KLBR_DISCORD_EVENT_MODE={other:?}; expected 'mentions' or 'all'"
 126 |             ),
 127 |         };
 128 | 
 129 |         Ok(Some(Self {
 130 |             token,
 131 |             source: std::env::var("KLBR_DISCORD_SOURCE").unwrap_or_else(|_| "discord".to_string()),
 132 |             wake_on_ambient: env_bool("KLBR_DISCORD_WAKE_ON_AMBIENT").unwrap_or(true),
 133 |             event_mode,
 134 |             include_bot_messages: env_bool("KLBR_DISCORD_INCLUDE_BOTS").unwrap_or(false),
 135 |             batch_interval: env_duration_secs("KLBR_DISCORD_BATCH_SECS", 8)?,
 136 |             channel_ids: env_channel_ids("KLBR_DISCORD_CHANNEL_IDS")?,
 137 |             pending_timeout: env_optional_duration_secs("KLBR_DISCORD_PENDING_TIMEOUT_SECS", 600)?,
 138 |         }))
 139 |     }
 140 | }

K05 — Discord intake, batching, and wake admission #

Current batching begins after receiving the first message. A directed message arriving inside an already-started ambient delay can wait for that delay. The plan preserves batching but separates durable admission from presentation and improves directed-message responsiveness.

Source: klbr-discord/src/lib.rs:275–336

 275 |     fn spawn_batcher(
 276 |         self: Arc<Self>,
 277 |         mut batch_rx: mpsc::Receiver<DiscordIncomingMessage>,
 278 |         interrupt_tx: mpsc::Sender<Interrupt>,
 279 |         output_tx: broadcast::Sender<AgentEvent>,
 280 |     ) -> JoinHandle<()> {
 281 |         tokio::spawn(async move {
 282 |             let mut batch_count = 0usize;
 283 |             while let Some(first) = batch_rx.recv().await {
 284 |                 let needs_wake = first.is_dm || first.mentions_bot;
 285 |                 let mut batch = vec![first];
 286 |                 if !needs_wake {
 287 |                     tokio::time::sleep(self.config.batch_interval).await;
 288 |                 }
 289 | 
 290 |                 while let Ok(message) = batch_rx.try_recv() {
 291 |                     batch.push(message);
 292 |                 }
 293 | 
 294 |                 let count = batch.len();
 295 |                 let include_instructions = batch_count % 6 == 0 || count >= 5;
 296 |                 batch_count += 1;
 297 |                 if let Some(timeout) = self.config.pending_timeout {
 298 |                     match self.inbox.demote_stale_pending(timeout.as_secs() as i64) {
 299 |                         Ok(n) if n > 0 => tracing::debug!(n, "auto-demoted stale pending items"),
 300 |                         Err(err) => tracing::warn!(?err, "discord pending auto-demote failed"),
 301 |                         _ => {}
 302 |                     }
 303 |                 }
 304 |                 self.note_batch_targets(&batch);
 305 |                 let new_pending_count = match self.note_inbox_batch(&batch) {
 306 |                     Ok(count) => count,
 307 |                     Err(err) => {
 308 |                         tracing::warn!(?err, "discord inbox update failed");
 309 |                         0
 310 |                     }
 311 |                 };
 312 |                 let pending = match self.inbox.pending_items(6) {
 313 |                     Ok(items) => items,
 314 |                     Err(err) => {
 315 |                         tracing::warn!(?err, "discord pending preview failed");
 316 |                         Vec::new()
 317 |                     }
 318 |                 };
 319 |                 let event = self.batch_event(&batch, &pending, include_instructions);
 320 |                 let should_wake = needs_wake
 321 |                     || should_wake_discord_batch(self.config.wake_on_ambient, new_pending_count);
 322 |                 if should_wake {
 323 |                     let interrupt = self.batch_interrupt(&batch, &pending, include_instructions);
 324 |                     if let Err(err) = interrupt_tx.send(interrupt).await {
 325 |                         tracing::warn!(?err, "discord batch send failed");
 326 |                         break;
 327 |                     }
 328 |                 }
 329 |                 let _ = output_tx.send(event);
 330 |                 tracing::debug!(
 331 |                     message_count = count,
 332 |                     new_pending_count,
 333 |                     needs_wake,
 334 |                     should_wake,
 335 |                     "processed discord message batch"
 336 |                 );

Source: klbr-discord/src/lib.rs:456–492

 456 |     async fn incoming_message(
 457 |         &self,
 458 |         message: &twilight_model::gateway::payload::incoming::MessageCreate,
 459 |     ) -> Option<DiscordIncomingMessage> {
 460 |         if message.author.id == self.bot_user_id {
 461 |             return None;
 462 |         }
 463 |         if message.author.bot && !self.config.include_bot_messages {
 464 |             return None;
 465 |         }
 466 | 
 467 |         let guild_id = message.guild_id.map(|id| id.to_string());
 468 |         let channel_id = message.channel_id.to_string();
 469 |         let channel_filter_enabled = !self.config.channel_ids.is_empty();
 470 |         let channel_allowed =
 471 |             channel_filter_enabled && self.config.channel_ids.contains(&channel_id);
 472 |         if channel_filter_enabled && !channel_allowed {
 473 |             return None;
 474 |         }
 475 |         self.note_seen_message(&channel_id, &message.id.to_string());
 476 | 
 477 |         let mentions_bot = message
 478 |             .mentions
 479 |             .iter()
 480 |             .any(|mention| mention.id == self.bot_user_id);
 481 |         let is_dm = message.guild_id.is_none();
 482 |         if !should_ingest_discord_message(
 483 |             self.config.event_mode,
 484 |             channel_filter_enabled,
 485 |             channel_allowed,
 486 |             is_dm,
 487 |             mentions_bot,
 488 |         ) {
 489 |             return None;
 490 |         }
 491 | 
 492 |         let conversation_id = self.conversation_id_for_channel(&channel_id);

K06 — Send, reply references, and acted marking #

The send path resolves a destination and source item and marks the associated item acted after a successful response. Reply-anchor selection currently mutates state before the send is known to have succeeded. The replacement ties consumption and disposition to confirmed effects.

Source: klbr-discord/src/lib.rs:589–640

 589 |     async fn try_discord_send(&self, args: Value) -> Result<String> {
 590 |         let channel_id: Id<ChannelMarker> = self.resolve_send_channel(&args)?;
 591 |         let conversation_id = self.conversation_id_for_channel(&channel_id.to_string());
 592 |         let content = string_arg(&args, "content")?;
 593 |         validate_message_content(content)?;
 594 |         let source_item_id = self.resolve_source_item_id(&args, &channel_id.to_string())?;
 595 |         let allowed_mentions = AllowedMentions::default();
 596 |         let reference_message_id = self
 597 |             .choose_reply_reference(&channel_id.to_string())
 598 |             .map(|id| id.parse::<u64>())
 599 |             .transpose()
 600 |             .context("stored Discord reply target was not a snowflake")?
 601 |             .map(Id::<MessageMarker>::new);
 602 | 
 603 |         let mut request = self
 604 |             .http
 605 |             .create_message(channel_id)
 606 |             .content(content)
 607 |             .allowed_mentions(Some(&allowed_mentions));
 608 |         if let Some(message_id) = reference_message_id {
 609 |             request = request.reply(message_id);
 610 |         }
 611 | 
 612 |         let message = request
 613 |             .await
 614 |             .context("discord create message failed")?
 615 |             .model()
 616 |             .await
 617 |             .context("failed to decode created Discord message")?;
 618 | 
 619 |         let mut result = format!(
 620 |             "sent conv:{} msg:{} reply_to:{} time:{}",
 621 |             conversation_id,
 622 |             message.id,
 623 |             reference_message_id
 624 |                 .map(|id| id.to_string())
 625 |                 .unwrap_or_else(|| "none".to_string()),
 626 |             format_discord_timestamp(message.timestamp)
 627 |         );
 628 |         if let Some(item_id) = source_item_id {
 629 |             self.inbox.mark(
 630 |                 &item_id,
 631 |                 "acted",
 632 |                 if reference_message_id.is_some() {
 633 |                     "replied"
 634 |                 } else {
 635 |                     "messaged"
 636 |                 },
 637 |                 Some("responded via discord_send"),
 638 |             )?;
 639 |             result.push_str(&format!(" item:{item_id} acted"));
 640 |         }

Source: klbr-discord/src/lib.rs:796–812

 796 |     fn resolve_source_item_id(&self, args: &Value, channel_id: &str) -> Result<Option<String>> {
 797 |         if let Some(item_id) = optional_string_arg(args, "source_item_id")? {
 798 |             return Ok(Some(normalize_item_id(item_id).to_string()));
 799 |         }
 800 | 
 801 |         let reply_target = {
 802 |             let state = self.state.lock().expect("discord runtime state poisoned");
 803 |             state.reply_target_by_channel.get(channel_id).cloned()
 804 |         };
 805 |         if let Some(message_id) = reply_target {
 806 |             if let Some(item_id) = self.inbox.pending_item_for_message(&message_id)? {
 807 |                 return Ok(Some(item_id));
 808 |             }
 809 |         }
 810 | 
 811 |         // fallback: most recent pending DM item for this channel
 812 |         self.inbox.pending_dm_item_for_channel(channel_id)

K07 — Attention statuses, aging, reactions, and explicit decisions #

Attention state is distinct from the event ingestion queue. DM/mention items become pending; aging produces seen/noted. The plan retains these visible semantics but records automatic aging as such rather than claiming a model read.

Source: klbr-discord/src/inbox.rs:51–88

  51 |             CREATE INDEX IF NOT EXISTS idx_discord_inbox_channel
  52 |                 ON discord_inbox_items(channel_id, status, last_seen_ts DESC);",
  53 |         )?;
  54 |         Ok(())
  55 |     }
  56 | 
  57 |     pub(super) fn upsert_pending(
  58 |         &self,
  59 |         message: &DiscordIncomingMessage,
  60 |     ) -> Result<Option<String>> {
  61 |         let Some(bucket) = pending_bucket(message) else {
  62 |             return Ok(None);
  63 |         };
  64 | 
  65 |         let conn = self.conn.lock().unwrap();
  66 |         conn.execute(
  67 |             "INSERT INTO discord_inbox_items (
  68 |                 item_id, conversation_id, channel_id, message_id, author_name,
  69 |                 content, bucket, status, action, message_ts
  70 |              ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'pending', 'none', ?8)
  71 |              ON CONFLICT(item_id) DO UPDATE SET
  72 |                 conversation_id = excluded.conversation_id,
  73 |                 channel_id = excluded.channel_id,
  74 |                 author_name = excluded.author_name,
  75 |                 content = excluded.content,
  76 |                 bucket = excluded.bucket,
  77 |                 last_seen_ts = unixepoch()",
  78 |             params![
  79 |                 message.message_id,
  80 |                 message.conversation_id,
  81 |                 message.channel_id,
  82 |                 message.message_id,
  83 |                 message.author_name,
  84 |                 message.content,
  85 |                 bucket,
  86 |                 message.timestamp,
  87 |             ],
  88 |         )?;

Source: klbr-discord/src/inbox.rs:110–126

 110 |              SET status = 'seen',
 111 |                  action = CASE WHEN action = 'none' THEN 'noted' ELSE action END,
 112 |                  decision_ts = unixepoch(),
 113 |                  last_seen_ts = unixepoch()
 114 |              WHERE status = 'pending'
 115 |                AND message_ts <= unixepoch() - ?1",
 116 |             params![older_than_secs],
 117 |         )?;
 118 |         Ok(changed)
 119 |     }
 120 | 
 121 |     pub(super) fn pending_item_for_message(&self, message_id: &str) -> Result<Option<String>> {
 122 |         self.conn
 123 |             .lock()
 124 |             .unwrap()
 125 |             .query_row(
 126 |                 "SELECT item_id

Source: klbr-discord/src/lib.rs:709–746

 709 |     async fn try_discord_react(&self, args: Value) -> Result<String> {
 710 |         let channel_id: Id<ChannelMarker> = self.resolve_channel(&args, true)?;
 711 |         let message_id: Id<MessageMarker> = normalized_id_arg(&args, "message_id")?;
 712 |         let emoji = string_arg(&args, "emoji")?;
 713 |         let reaction = parse_reaction(emoji)?;
 714 | 
 715 |         self.http
 716 |             .create_reaction(channel_id, message_id, &reaction)
 717 |             .await
 718 |             .context("discord create reaction failed")?;
 719 |         let conversation_id = self
 720 |             .existing_conversation_id_for_channel(&channel_id.to_string())
 721 |             .unwrap_or_else(|| channel_id.to_string());
 722 | 
 723 |         Ok(format!("reacted conv:{conversation_id} msg:{message_id}"))
 724 |     }
 725 | 
 726 |     async fn discord_mark(&self, args: Value) -> String {
 727 |         match self.try_discord_mark(args).await {
 728 |             Ok(result) => result,
 729 |             Err(err) => format!("error: {err}"),
 730 |         }
 731 |     }
 732 | 
 733 |     async fn try_discord_mark(&self, args: Value) -> Result<String> {
 734 |         let item_id = string_arg(&args, "item_id")?;
 735 |         let status = args["status"].as_str().unwrap_or("seen");
 736 |         let action = args["action"].as_str().unwrap_or_else(|| match status {
 737 |             "acted" => "replied",
 738 |             "ignored" => "dismissed",
 739 |             _ => "none",
 740 |         });
 741 |         let note = optional_string_arg(&args, "note")?;
 742 |         self.inbox.mark(item_id, status, action, note)?;
 743 |         Ok(format!(
 744 |             "marked item:{item_id} status:{status} action:{action}"
 745 |         ))
 746 |     }

Source: klbr-discord/src/lib.rs:908–938

 908 | fn pending_bucket(message: &DiscordIncomingMessage) -> Option<&'static str> {
 909 |     if message.author_is_bot {
 910 |         return None;
 911 |     }
 912 |     if message.is_dm {
 913 |         return Some("dm");
 914 |     }
 915 |     if message.mentions_bot {
 916 |         return Some("mention");
 917 |     }
 918 |     None
 919 | }
 920 | 
 921 | fn should_ingest_discord_message(
 922 |     event_mode: DiscordEventMode,
 923 |     channel_filter_enabled: bool,
 924 |     channel_allowed: bool,
 925 |     is_dm: bool,
 926 |     mentions_bot: bool,
 927 | ) -> bool {
 928 |     if is_dm || mentions_bot {
 929 |         return true;
 930 |     }
 931 |     if channel_filter_enabled && channel_allowed {
 932 |         return true;
 933 |     }
 934 |     event_mode == DiscordEventMode::All
 935 | }
 936 | 
 937 | fn should_wake_discord_batch(wake_on_ambient: bool, new_pending_count: usize) -> bool {
 938 |     wake_on_ambient || new_pending_count > 0

K08 — Short conversation aliases, implicit destination, and reply threading #

The current adapter uses in-memory short labels and active-channel state. The plan preserves convenient aliases and smart reply behavior while making identities durable and default destinations immutable per attention frame.

Source: klbr-discord/src/lib.rs:862–906

 862 | fn choose_reply_reference_from_state(state: &mut DiscordState, channel_id: &str) -> Option<String> {
 863 |     let target = state.reply_target_by_channel.remove(channel_id)?;
 864 |     let latest = state.latest_message_by_channel.get(channel_id);
 865 |     if latest.is_some_and(|latest| latest != &target) {
 866 |         Some(target)
 867 |     } else {
 868 |         None
 869 |     }
 870 | }
 871 | 
 872 | fn conversation_id_for_channel_from_state(state: &mut DiscordState, channel_id: &str) -> String {
 873 |     if let Some(id) = state.conversation_by_channel.get(channel_id) {
 874 |         return id.clone();
 875 |     }
 876 | 
 877 |     state.next_conversation_id += 1;
 878 |     let id = format!("d{}", state.next_conversation_id);
 879 |     state
 880 |         .conversation_by_channel
 881 |         .insert(channel_id.to_string(), id.clone());
 882 |     state
 883 |         .channel_by_conversation
 884 |         .insert(id.clone(), channel_id.to_string());
 885 |     id
 886 | }
 887 | 
 888 | fn note_batch_targets_in_state<'a>(
 889 |     state: &mut DiscordState,
 890 |     latest_by_channel: HashMap<&'a str, &'a str>,
 891 | ) {
 892 |     state.active_channel_id = if latest_by_channel.len() == 1 {
 893 |         latest_by_channel
 894 |             .keys()
 895 |             .next()
 896 |             .map(|channel_id| (*channel_id).to_string())
 897 |     } else {
 898 |         None
 899 |     };
 900 | 
 901 |     for (channel_id, message_id) in latest_by_channel {
 902 |         state
 903 |             .reply_target_by_channel
 904 |             .insert(channel_id.to_string(), message_id.to_string());
 905 |     }
 906 | }

Source: klbr-discord/src/lib.rs:948–990

 948 | fn resolve_channel_arg_from_state(
 949 |     state: &DiscordState,
 950 |     args: &Value,
 951 |     allow_active: bool,
 952 | ) -> Result<String> {
 953 |     if let Some(conversation_id) = optional_string_arg(args, "conversation_id")? {
 954 |         return resolve_conversation_channel_from_state(state, conversation_id);
 955 |     }
 956 | 
 957 |     if let Some(channel_id) = optional_string_arg(args, "channel_id")? {
 958 |         if parse_discord_id::<ChannelMarker>(channel_id, "Discord channel").is_ok() {
 959 |             return Ok(channel_id.to_string());
 960 |         }
 961 |         return resolve_conversation_channel_from_state(state, channel_id).with_context(|| {
 962 |             format!(
 963 |                 "channel_id {channel_id:?} is neither a Discord snowflake nor a known conversation id"
 964 |             )
 965 |         });
 966 |     }
 967 | 
 968 |     if allow_active {
 969 |         if let Some(channel_id) = &state.active_channel_id {
 970 |             return Ok(channel_id.clone());
 971 |         }
 972 |     }
 973 | 
 974 |     anyhow::bail!(
 975 |         "conversation_id or channel_id is required when the current Discord batch spans multiple conversations"
 976 |     )
 977 | }
 978 | 
 979 | fn resolve_conversation_channel_from_state(
 980 |     state: &DiscordState,
 981 |     raw_conversation_id: &str,
 982 | ) -> Result<String> {
 983 |     let conversation_id = normalize_conversation_id(raw_conversation_id);
 984 |     let Some(channel_id) = state.channel_by_conversation.get(conversation_id) else {
 985 |         anyhow::bail!("unknown Discord conversation_id {conversation_id:?}");
 986 |     };
 987 |     Ok(channel_id.clone())
 988 | }
 989 | 
 990 | fn normalize_conversation_id(raw: &str) -> &str {

K09 — Discord tool surface and stale description #

The old send description says ordinary assistant text goes to local chat; K01–K03 demonstrate why this wording must not survive the migration. Existing operations include send, backread, reaction, conversation listing, and pending-item marking.

Source: klbr-discord/src/tool_defs.rs:1–124

   1 | use klbr_core::models::ToolDef;
   2 | use serde_json::json;
   3 | 
   4 | pub(super) fn discord_send_def() -> ToolDef {
   5 |     ToolDef::function(
   6 |         "discord_send",
   7 |         "send a Discord message. use this when a Discord response is useful; many ambient batches need no reply. plain assistant text only goes to the local chat. conversation_id is optional when the current Discord batch has a single conversation. pass source_item_id when responding to a pending item; successful sends mark that item acted, so do not call discord_mark afterward.",
   8 |         json!({
   9 |             "type": "object",
  10 |             "properties": {
  11 |                 "conversation_id": {
  12 |                     "type": "string",
  13 |                     "description": "Discord conversation id from the batch, like d1 or conv:d1; omit only for a single-conversation batch"
  14 |                 },
  15 |                 "content": {
  16 |                     "type": "string",
  17 |                     "description": "message content to send, max 2000 characters"
  18 |                 },
  19 |                 "source_item_id": {
  20 |                     "type": "string",
  21 |                     "description": "optional pending inbox item id shown as msg:<id>; marks it acted after sending"
  22 |                 }
  23 |             },
  24 |             "required": ["content"]
  25 |         }),
  26 |     )
  27 | }
  28 | 
  29 | pub(super) fn discord_fetch_history_def() -> ToolDef {
  30 |     ToolDef::function(
  31 |         "discord_fetch_history",
  32 |         "fetch recent Discord message history for a conversation. use this when the current batch lacks enough context. conversation_id is optional when the current batch has a single conversation.",
  33 |         json!({
  34 |             "type": "object",
  35 |             "properties": {
  36 |                 "conversation_id": {
  37 |                     "type": "string",
  38 |                     "description": "Discord conversation id from the batch, like d1 or conv:d1; omit only for a single-conversation batch"
  39 |                 },
  40 |                 "before_message_id": {
  41 |                     "type": "string",
  42 |                     "description": "optional msg:<id> from the batch; fetch messages before it"
  43 |                 },
  44 |                 "after_message_id": {
  45 |                     "type": "string",
  46 |                     "description": "optional msg:<id> from the batch; fetch messages after it"
  47 |                 },
  48 |                 "limit": {
  49 |                     "type": "integer",
  50 |                     "description": "number of messages to fetch, 1-100; default 20"
  51 |                 }
  52 |             },
  53 |             "required": []
  54 |         }),
  55 |     )
  56 | }
  57 | 
  58 | pub(super) fn discord_react_def() -> ToolDef {
  59 |     ToolDef::function(
  60 |         "discord_react",
  61 |         "add a reaction to a Discord message. prefer real unicode emoji like 😍, 👍, or ❤️. only use a custom emoji string like <:name:id> when the exact string came from Discord; never invent custom emoji ids. conversation_id is optional when the current Discord batch has a single conversation.",
  62 |         json!({
  63 |             "type": "object",
  64 |             "properties": {
  65 |                 "conversation_id": {
  66 |                     "type": "string",
  67 |                     "description": "Discord conversation id from the batch, like d1 or conv:d1; omit only for a single-conversation batch"
  68 |                 },
  69 |                 "message_id": {
  70 |                     "type": "string",
  71 |                     "description": "msg:<id> of the message to react to, as shown in the batch"
  72 |                 },
  73 |                 "emoji": {
  74 |                     "type": "string",
  75 |                     "description": "unicode emoji is preferred; custom emoji must be an exact Discord form <:name:id> / <a:name:id>, copied from Discord rather than guessed"
  76 |                 }
  77 |             },
  78 |             "required": ["message_id", "emoji"]
  79 |         }),
  80 |     )
  81 | }
  82 | 
  83 | pub(super) fn discord_list_conversations_def() -> ToolDef {
  84 |     ToolDef::function(
  85 |         "discord_list_conversations",
  86 |         "list known Discord conversations with their ids, type (dm/guild), and last activity. use this to find a conversation_id when it is not in the current batch.",
  87 |         json!({
  88 |             "type": "object",
  89 |             "properties": {},
  90 |             "required": []
  91 |         }),
  92 |     )
  93 | }
  94 | 
  95 | pub(super) fn discord_mark_def() -> ToolDef {
  96 |     ToolDef::function(
  97 |         "discord_mark",
  98 |         "mark a pending Discord inbox item as seen, acted, or ignored so future batches remember the decision. use this when you are not sending a Discord response, especially ignored when you deliberately choose not to respond. successful discord_send already marks the item acted.",
  99 |         json!({
 100 |             "type": "object",
 101 |             "properties": {
 102 |                 "item_id": {
 103 |                     "type": "string",
 104 |                     "description": "pending Discord inbox message id, either raw snowflake or the displayed msg:<id>"
 105 |                 },
 106 |                 "status": {
 107 |                     "type": "string",
 108 |                     "enum": ["pending", "seen", "acted", "ignored"],
 109 |                     "description": "new item status"
 110 |                 },
 111 |                 "action": {
 112 |                     "type": "string",
 113 |                     "enum": ["none", "replied", "messaged", "dismissed", "noted"],
 114 |                     "description": "optional decision/action label"
 115 |                 },
 116 |                 "note": {
 117 |                     "type": "string",
 118 |                     "description": "optional short decision note"
 119 |                 }
 120 |             },
 121 |             "required": ["item_id", "status"]
 122 |         }),
 123 |     )
 124 | }

K10 — Operator preemption versus queued external events #

The existing interrupt contract distinguishes operator/management preemption from external events. Preserve that priority, not the single-slot event-loss implementation.

Source: klbr-core/src/interrupt.rs:1–22

   1 | use crate::harness_block::{self, DEFAULT_FORMAT_STYLE};
   2 | use tokio::sync::mpsc;
   3 | 
   4 | #[derive(Debug, Clone)]
   5 | pub enum Interrupt {
   6 |     Message { source: String, content: String },
   7 |     ExternalEvent(ExternalEvent),
   8 |     Reset,
   9 |     Compact,
  10 |     DebugReflectRequest,
  11 |     UpdateSoul { content: String },
  12 | }
  13 | 
  14 | #[derive(Debug, Clone)]
  15 | pub struct ExternalEvent {
  16 |     pub source: String,
  17 |     pub conversation_id: String,
  18 |     pub content: String,
  19 |     pub author_id: Option<String>,
  20 |     pub author_name: Option<String>,
  21 |     pub message_id: Option<String>,
  22 |     pub timestamp: Option<i64>,

Source: klbr-core/src/interrupt.rs:86–97

  86 |     /// Returns true if this interrupt should preempt an in-progress LLM stream or tool execution.
  87 |     /// Operator messages (from TUI, web, etc.) and management commands are preemptive.
  88 |     /// Automated external events are not — they queue behind the current turn.
  89 |     pub fn is_preemptive(&self) -> bool {
  90 |         matches!(
  91 |             self,
  92 |             Interrupt::Message { .. }
  93 |                 | Interrupt::Reset
  94 |                 | Interrupt::Compact
  95 |                 | Interrupt::UpdateSoul { .. }
  96 |         )
  97 |     }

K11 — Wait-and-continue semantics #

The old wait is a special runtime action with bounded duration and input interruption. The new Python spelling is a terminal yield backed by a durable wakeup, not a durable suspended Python stack. Rejecting invalid or self-spinning delays is a deliberate change.

Source: klbr-core/src/tools/wait_and_continue.rs:12–39

  12 | 
  13 | const MAX_WAIT_MS: u64 = 600_000;
  14 | 
  15 | pub fn tool() -> Tool {
  16 |     Tool::new(definition(), exec)
  17 | }
  18 | 
  19 | fn definition() -> ToolDef {
  20 |     ToolDef::function(
  21 |         "wait_and_continue",
  22 |         "yield control and wait for an incoming event or a bounded timeout. if an event arrives, the harness injects it before the next assistant turn. if no event arrives before the timeout, the turn suspends silently. requires timeout_ms (max 600000) or seconds.",
  23 |         json!({
  24 |             "type": "object",
  25 |             "properties": {
  26 |                 "timeout_ms": {
  27 |                     "type": "integer",
  28 |                     "description": "milliseconds to wait before continuing, max 600000"
  29 |                 },
  30 |                 "seconds": {
  31 |                     "type": "number",
  32 |                     "description": "optional seconds form; ignored if timeout_ms is provided"
  33 |                 },
  34 |                 "reason": {
  35 |                     "type": "string",
  36 |                     "description": "optional short reason for waiting"
  37 |                 }
  38 |             }
  39 |         }),

Source: klbr-core/src/tools/wait_and_continue.rs:61–152

  61 |         format!("waited {waited_ms}ms; continue")
  62 |     } else {
  63 |         format!("waited {waited_ms}ms ({reason}); continue")
  64 |     }
  65 | }
  66 | 
  67 | pub fn timeout_duration(args: &serde_json::Value) -> Result<Duration, String> {
  68 |     if let Some(timeout_ms) = args["timeout_ms"].as_u64() {
  69 |         return Ok(Duration::from_millis(timeout_ms.min(MAX_WAIT_MS)));
  70 |     }
  71 | 
  72 |     if !args["seconds"].is_null() {
  73 |         let seconds = args["seconds"]
  74 |             .as_f64()
  75 |             .ok_or_else(|| "seconds must be a number".to_string())?;
  76 |         if !seconds.is_finite() {
  77 |             return Err("seconds must be finite".to_string());
  78 |         }
  79 |         let millis = (seconds.max(0.0) * 1000.0).round() as u64;
  80 |         return Ok(Duration::from_millis(millis.min(MAX_WAIT_MS)));
  81 |     }
  82 | 
  83 |     Err("duration is required (either 'timeout_ms' or 'seconds' must be specified)".to_string())
  84 | }
  85 | 
  86 | pub enum RuntimeWaitResult {
  87 |     Timeout(String),
  88 |     Incoming {
  89 |         result: String,
  90 |         interrupt: Interrupt,
  91 |     },
  92 |     Reset,
  93 |     Compact,
  94 |     UpdateSoul(String),
  95 | }
  96 | 
  97 | pub async fn execute_runtime(
  98 |     arguments: &str,
  99 |     rx: &mut mpsc::Receiver<Interrupt>,
 100 | ) -> RuntimeWaitResult {
 101 |     let args: serde_json::Value = serde_json::from_str(arguments).unwrap_or_default();
 102 |     let timeout = match timeout_duration(&args) {
 103 |         Ok(timeout) => timeout,
 104 |         Err(err) => return RuntimeWaitResult::Timeout(format!("error: {err}")),
 105 |     };
 106 | 
 107 |     let start_time = std::time::Instant::now();
 108 |     let mut sleep = Box::pin(tokio::time::sleep(timeout));
 109 |     tokio::select! {
 110 |         _ = &mut sleep => {
 111 |             let elapsed = start_time.elapsed();
 112 |             let elapsed_str = format_duration_human(elapsed);
 113 |             let current_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string();
 114 |             RuntimeWaitResult::Timeout(format!(
 115 |                 "waited {elapsed_str}; no incoming event arrived; current time: {current_time}"
 116 |             ))
 117 |         }
 118 |         interrupt = rx.recv() => {
 119 |             let elapsed = start_time.elapsed();
 120 |             let elapsed_str = format_duration_human(elapsed);
 121 |             let current_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string();
 122 |             match interrupt {
 123 |                 Some(Interrupt::Reset) => RuntimeWaitResult::Reset,
 124 |                 Some(Interrupt::Compact) => RuntimeWaitResult::Compact,
 125 |                 Some(Interrupt::UpdateSoul { content }) => RuntimeWaitResult::UpdateSoul(content),
 126 |                 Some(Interrupt::DebugReflectRequest) => RuntimeWaitResult::Timeout(
 127 |                     format!("debug reflection request received while waiting; current time: {current_time}; elapsed since last turn: {elapsed_str}; try again after this turn")
 128 |                 ),
 129 |                 Some(interrupt) => {
 130 |                     let source = interrupt.source_tag().to_string();
 131 |                     RuntimeWaitResult::Incoming {
 132 |                         result: format!(
 133 |                             "interrupted early by incoming {source} event after {elapsed_str}; current time: {current_time}"
 134 |                         ),
 135 |                         interrupt,
 136 |                     }
 137 |                 }
 138 |                 None => RuntimeWaitResult::Timeout(format!("interrupt channel closed; current time: {current_time}; elapsed since last turn: {elapsed_str}; continue")),
 139 |             }
 140 |         }
 141 |     }
 142 | }
 143 | 
 144 | pub fn wait_timeout_without_event(result: &str) -> bool {
 145 |     result.contains("no incoming event arrived")
 146 | }
 147 | 
 148 | pub fn format_duration_human(d: Duration) -> String {
 149 |     let secs = d.as_secs();
 150 |     if secs == 0 {
 151 |         return format!("{}ms", d.as_millis());
 152 |     }

K12 — Continuous autonomous loop and tool-name lifecycle #

The loop can act without a new user request. Its special handling of tool names must be replaced coherently rather than leaving a second orchestration model under the Python executor.

Source: klbr-core/src/agent.rs:278–342

 278 | 
 279 |                     handle_stream_failure_backoff(&ctx, &mut failure_count).await;
 280 | 
 281 |                     while let Some(curr_int) = next_interrupt {
 282 |                         if !matches!(
 283 |                             curr_int,
 284 |                             Interrupt::Message { .. } | Interrupt::ExternalEvent(_)
 285 |                         ) {
 286 |                             self.handle_system_interrupt(
 287 |                                 curr_int,
 288 |                                 &mut ctx,
 289 |                                 &mut turn_count,
 290 |                                 &mut runtime_soul,
 291 |                                 &tool_ctx,
 292 |                                 &llm,
 293 |                             )
 294 |                             .await?;
 295 |                             break;
 296 |                         }
 297 |                         last_event_at = std::time::Instant::now();
 298 |                         skip_idle_nudge = true;
 299 |                         next_interrupt = self
 300 |                             .process_interrupt_and_run_turn(
 301 |                                 curr_int,
 302 |                                 &llm,
 303 |                                 &tool_ctx,
 304 |                                 &mut ctx,
 305 |                                 &mut turn_count,
 306 |                                 &mut runtime_soul,
 307 |                                 &router,
 308 |                                 &mut consecutive_waits,
 309 |                             )
 310 |                             .await?;
 311 | 
 312 |                         if handle_stream_failure_backoff(&ctx, &mut failure_count).await {
 313 |                             break;
 314 |                         }
 315 |                     }
 316 |                 } else {
 317 |                     // Nothing pending. Only inject the nudge if:
 318 |                     // - we didn't just process a real event (model gets one free turn to
 319 |                     //   call wait_and_continue on its own before we prompt it), AND
 320 |                     // - the model didn't already end on a wait timeout (it knows what to do).
 321 |                     let should_nudge = !skip_idle_nudge && !is_completed_wait_tool_result(&ctx);
 322 |                     skip_idle_nudge = false;
 323 |                     if should_nudge {
 324 |                         let elapsed = last_event_at.elapsed();
 325 |                         push_visible_nudge(&mut ctx, &self.output, &idle_nudge_message(elapsed));
 326 |                     }
 327 |                     let mut next_interrupt = self
 328 |                         .run_turn(
 329 |                             &llm,
 330 |                             &tool_ctx,
 331 |                             &mut ctx,
 332 |                             &mut turn_count,
 333 |                             &mut runtime_soul,
 334 |                             None,
 335 |                             false,
 336 |                             false,
 337 |                             &mut consecutive_waits,
 338 |                         )
 339 |                         .await?;
 340 | 
 341 |                     handle_stream_failure_backoff(&ctx, &mut failure_count).await;
 342 | 

Source: klbr-core/src/agent/turn.rs:160–222

 160 |             if name == "restart_harness" {
 161 |                 should_restart = true;
 162 |             }
 163 |             if name != "wait_and_continue" {
 164 |                 *consecutive_waits = 0;
 165 |             }
 166 |             if name == "discord_send" {
 167 |                 *discord_send_called = true;
 168 |             }
 169 |             if name == "local_send" {
 170 |                 *local_send_called = true;
 171 |             }
 172 |             let args = call.function.arguments.clone();
 173 |             if name != "local_send" {
 174 |                 let _ = self.output.send(AgentEvent::ToolCall {
 175 |                     name: name.clone(),
 176 |                     args: args.clone(),
 177 |                 });
 178 |             }
 179 | 
 180 |             let wait_result = if name == "wait_and_continue" {
 181 |                 Some(tools::execute_wait_and_continue(&args, &mut self.rx).await)
 182 |             } else {
 183 |                 None
 184 |             };
 185 |             let mut incoming_after_tool = None;
 186 |             let mut suspend_after_wait = false;
 187 |             let mut sent_local_content = None;
 188 |             let result = if name == "local_send" {
 189 |                 let delivery = tools::local_send_runtime_delivery(&args);
 190 |                 if let Some(err) = &delivery.error {
 191 |                     tracing::warn!(err = %err, "local_send call had invalid arguments");
 192 |                 }
 193 |                 sent_local_content = delivery.sent_content;
 194 |                 delivery.result
 195 |             } else {
 196 |                 match wait_result {
 197 |                     Some(WaitAndContinueResult::Timeout(mut result)) => {
 198 |                         suspend_after_wait = tools::wait_timeout_without_event(&result);
 199 |                         if suspend_after_wait {
 200 |                             *consecutive_waits += 1;
 201 |                             if *consecutive_waits >= 5 && *consecutive_waits % 5 == 0 {
 202 |                                 let nudge = format!(
 203 |                                     "\n\nnudge: you have called wait_and_continue {} consecutive times without taking other actions. to conserve local resources, please increase the wait duration in your next wait_and_continue call (e.g. wait_and_continue(duration=\"10m\")).",
 204 |                                     *consecutive_waits
 205 |                                 );
 206 |                                 result.push_str(&nudge);
 207 |                             }
 208 |                         } else {
 209 |                             *consecutive_waits = 0;
 210 |                         }
 211 |                         result
 212 |                     }
 213 |                     Some(WaitAndContinueResult::Incoming { result, interrupt }) => {
 214 |                         incoming_after_tool = Some(interrupt);
 215 |                         result
 216 |                     }
 217 |                     Some(WaitAndContinueResult::Reset) => {
 218 |                         ctx.clear();
 219 |                         *turn_count = 0;
 220 |                         *runtime_soul = self.sys_prompt()?;
 221 |                         ctx.update_soul(runtime_soul, &[]);
 222 |                         save_context_snapshot(&self.memory, ctx);

K13 — Soul updates and host restart #

The checked code combines stored soul with compiled instructions, supports UpdateSoul, and has a restart tool. The redesign keeps editable identity and explicit upgrades while splitting prompt updates, kernel replacement, and host deployment into distinct operations.

Source: klbr-core/src/agent.rs:60–70

  60 |     pub fn sys_prompt(&self) -> Result<String> {
  61 |         let soul = self
  62 |             .memory
  63 |             .soul_text()?
  64 |             .unwrap_or_else(|| crate::config::DEFAULT_SOUL.to_string());
  65 |         let instructions = include_str!("instructions.md").replace("{{name}}", &self.config.name);
  66 |         Ok(format!("{soul}\n\n{instructions}"))
  67 |     }
  68 | 
  69 |     pub async fn run(mut self) -> Result<()> {
  70 |         let mut runtime_soul = self.sys_prompt()?;

Source: klbr-core/src/agent.rs:395–413

 395 |             Interrupt::UpdateSoul { content } => {
 396 |                 self.memory.set_soul_text(&content)?;
 397 |                 *runtime_soul = self.sys_prompt()?;
 398 |                 let pinned = self.memory.pinned_memories().unwrap_or_default();
 399 |                 ctx.update_soul(runtime_soul, &pinned);
 400 |                 save_context_snapshot(&self.memory, ctx);
 401 |                 let _ = self.output.send(AgentEvent::Status("soul updated".into()));
 402 |             }
 403 |             Interrupt::Reset => {
 404 |                 ctx.clear();
 405 |                 *turn_count = 0;
 406 |                 *runtime_soul = self.sys_prompt()?;
 407 |                 ctx.update_soul(runtime_soul, &[]);
 408 |                 save_context_snapshot(&self.memory, ctx);
 409 |                 let _ = self.output.send(AgentEvent::Reset);
 410 |                 let _ = self.output.send(AgentEvent::Status("context reset".into()));
 411 |             }
 412 |             Interrupt::Compact => {
 413 |                 let _ = self.output.send(AgentEvent::Status("compacting...".into()));

Source: klbr-core/src/tools/restart_harness.rs:12–58

  12 | }
  13 | 
  14 | fn definition() -> ToolDef {
  15 |     ToolDef::function(
  16 |         "restart_harness",
  17 |         "restart the klbr agent daemon/harness. use this after making modifications to the harness codebase (which you have compiled successfully) to reload the harness with the new binary. this replaces the current process image and preserves state via database snapshotted context.",
  18 |         json!({
  19 |             "type": "object",
  20 |             "properties": {}
  21 |         }),
  22 |     )
  23 | }
  24 | 
  25 | fn exec(
  26 |     _args: serde_json::Value,
  27 |     _ctx: ToolContext,
  28 | ) -> Pin<Box<dyn Future<Output = String> + Send>> {
  29 |     Box::pin(async { "harness restarting... websocket will reconnect.".to_string() })
  30 | }
  31 | 
  32 | #[cfg(unix)]
  33 | pub fn perform() -> ! {
  34 |     let exe = match std::env::current_exe() {
  35 |         Ok(path) => path,
  36 |         Err(e) => {
  37 |             tracing::error!("failed to get current executable path: {e}");
  38 |             std::process::exit(1);
  39 |         }
  40 |     };
  41 |     let args: Vec<String> = std::env::args().skip(1).collect();
  42 | 
  43 |     let now = std::time::SystemTime::now()
  44 |         .duration_since(std::time::UNIX_EPOCH)
  45 |         .map(|d| d.as_secs())
  46 |         .unwrap_or_default();
  47 | 
  48 |     tracing::info!("restarting harness: {:?} with args {:?}", exe, args);
  49 | 
  50 |     use std::os::unix::process::CommandExt;
  51 |     let mut cmd = std::process::Command::new(exe);
  52 |     cmd.args(&args);
  53 |     cmd.env("KLBR_RESTART_TIMESTAMP", now.to_string());
  54 | 
  55 |     let err = cmd.exec();
  56 |     tracing::error!("exec failed: {err}");
  57 |     std::process::exit(1);
  58 | }

K14 — Code-intelligence vocabulary and mutation limitations #

Preserve source ranges, symbols and diagnostics in Python. Definition/reference matching is best-effort. The selected edit path writes the whole updated file and replaces the tag range; it is not a version-checked compiler-backed body edit.

Source: klbr-core/src/code_intel.rs:43–77

  43 | pub struct SourceRange {
  44 |     pub start_byte: usize,
  45 |     pub end_byte: usize,
  46 |     pub start_line: usize,
  47 |     pub start_column: usize,
  48 |     pub end_line: usize,
  49 |     pub end_column: usize,
  50 | }
  51 | 
  52 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
  53 | #[serde(rename_all = "snake_case")]
  54 | pub enum SymbolRole {
  55 |     Definition,
  56 |     Reference,
  57 | }
  58 | 
  59 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
  60 | pub struct CodeSymbol {
  61 |     pub name: String,
  62 |     pub name_path: Vec<String>,
  63 |     pub kind: String,
  64 |     pub role: SymbolRole,
  65 |     pub range: SourceRange,
  66 |     pub name_range: SourceRange,
  67 |     pub docs: Option<String>,
  68 | }
  69 | 
  70 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
  71 | pub struct SymbolExtraction {
  72 |     pub language: ResolvedLanguage,
  73 |     pub queries: QuerySupport,
  74 |     pub status: ParseStatus,
  75 |     pub symbols: Vec<CodeSymbol>,
  76 | }
  77 | 

Source: klbr-core/src/code_intel.rs:113–130

 113 | pub fn definitions_named<'a>(symbols: &'a [CodeSymbol], name: &str) -> Vec<&'a CodeSymbol> {
 114 |     let name = name.trim();
 115 |     symbols
 116 |         .iter()
 117 |         .filter(|symbol| {
 118 |             symbol.role == SymbolRole::Definition
 119 |                 && (symbol.name == name || symbol_path(symbol) == name)
 120 |         })
 121 |         .collect()
 122 | }
 123 | 
 124 | pub fn references_named<'a>(symbols: &'a [CodeSymbol], name: &str) -> Vec<&'a CodeSymbol> {
 125 |     let name = name.trim();
 126 |     symbols
 127 |         .iter()
 128 |         .filter(|symbol| symbol.role == SymbolRole::Reference && symbol.name == name)
 129 |         .collect()
 130 | }

Source: klbr-core/src/tools/symbol_edit.rs:29–54

  29 | fn replace_definition() -> ToolDef {
  30 |     ToolDef::function(
  31 |         "replace_symbol_body",
  32 |         "replace the exact source range for one file-local tree-sitter symbol",
  33 |         json!({
  34 |             "type": "object",
  35 |             "properties": {
  36 |                 "path": {
  37 |                     "type": "string",
  38 |                     "description": "absolute or relative path to the file"
  39 |                 },
  40 |                 "name_path": {
  41 |                     "type": "string",
  42 |                     "description": "symbol name or file-local path"
  43 |                 },
  44 |                 "body": {
  45 |                     "type": "string",
  46 |                     "description": "replacement source body"
  47 |                 },
  48 |                 "language": {
  49 |                     "type": "string",
  50 |                     "description": "optional tree-sitter language name override"
  51 |                 }
  52 |             },
  53 |             "required": ["path", "name_path", "body"]
  54 |         }),

Source: klbr-core/src/tools/symbol_edit.rs:155–205

 155 | async fn execute_edit(args: serde_json::Value, edit: SymbolEdit) -> String {
 156 |     let path = match args["path"].as_str() {
 157 |         Some(path) => path,
 158 |         None => return "error: missing required arg 'path'".into(),
 159 |     };
 160 |     let name_path = match args["name_path"].as_str().map(str::trim) {
 161 |         Some(name_path) if !name_path.is_empty() => name_path,
 162 |         None => return "error: missing required arg 'name_path'".into(),
 163 |         Some(_) => return "error: missing required arg 'name_path'".into(),
 164 |     };
 165 |     let language = args["language"].as_str();
 166 | 
 167 |     let source = match tokio::fs::read_to_string(path).await {
 168 |         Ok(source) => source,
 169 |         Err(err) => return format!("error: {err}"),
 170 |     };
 171 |     let updated = match edit_source(path, &source, language, name_path, edit) {
 172 |         Ok(updated) => updated,
 173 |         Err(err) => return err,
 174 |     };
 175 |     match tokio::fs::write(path, updated).await {
 176 |         Ok(()) => "OK".into(),
 177 |         Err(err) => format!("error: {err}"),
 178 |     }
 179 | }
 180 | 
 181 | fn edit_source(
 182 |     path: &str,
 183 |     source: &str,
 184 |     language: Option<&str>,
 185 |     name_path: &str,
 186 |     edit: SymbolEdit,
 187 | ) -> Result<String, String> {
 188 |     let extraction =
 189 |         extract_symbols(Some(path), source, language).map_err(|err| format!("error: {err}"))?;
 190 |     let matches = matching_symbols(&extraction.symbols, name_path, false);
 191 |     if matches.is_empty() {
 192 |         return Err(format!("error: symbol not found: {name_path}"));
 193 |     }
 194 |     if matches.len() > 1 {
 195 |         let choices = format_symbol_choices(&matches, 12);
 196 |         return Err(format!("error: ambiguous symbol: {choices}"));
 197 |     }
 198 | 
 199 |     let range = &matches[0].range;
 200 |     validate_range(source, range)?;
 201 |     Ok(match edit {
 202 |         SymbolEdit::Replace { body } => replace_range(source, range, &body),
 203 |         SymbolEdit::InsertBefore { text } => insert_before_range(source, range, &text),
 204 |         SymbolEdit::InsertAfter { text } => insert_after_range(source, range, &text),
 205 |     })

K15 — Lucid: meaningful states, ownership, vocabulary, and behavior changes #

These passages inform the proposed separation of history, operational state, live computation and released behavior. A shorter source tree is not the objective; neither preserving incidental bugs nor gratuitously changing semantics is justified.

Source: plugins/lucid-code/skills/lucid-code/SKILL.md:41–60

  41 | ## Represent truth
  42 | 
  43 | - Parse weak or untrusted values at the boundary into representations that preserve
  44 |   what was learned.
  45 | - Represent every meaningful state, including legitimate drafts, pending work,
  46 |   unknowns, failures, and partial external input. Exclude combinations for which the
  47 |   domain has no valid behavior.
  48 | - Choose the most precise representation that makes legal operations direct and illegal
  49 |   combinations unavailable. Opaque or refined values, role types, closed variants,
  50 |   typestate, phantom parameters, smart constructors, builders, and schemas are ordinary
  51 |   tools. A type may be valuable solely because it carries a role, unit, identity, proof,
  52 |   capability, or protocol state. Expose type machinery when callers can use the
  53 |   distinction or the compiler can enforce it.
  54 | - Make state authority explicit. When the domain has one source of truth, derive
  55 |   secondary views unless caching, materialization, indexing, or boundary semantics make
  56 |   another representation better. When authority is replicated or distributed, encode
  57 |   ownership, merge, and consistency semantics rather than pretending it is singular.
  58 | - Expose failure and partiality where callers can act on them. Prefer total public and
  59 |   domain APIs when no valid caller can satisfy a hidden precondition. A partial interior
  60 |   operation is fine when its proof is local and stable and failure means an internal

Source: plugins/lucid-code/skills/lucid-code/SKILL.md:110–141

 110 | 
 111 | A refactor may change observable mechanics and even behavior when the old behavior is
 112 | buggy, incidental, or superseded by the requested design. Understand the change,
 113 | migrate affected APIs and tests, and make a material contract change explicit. Do not
 114 | preserve an inefficiency because it is observable, and do not change behavior casually
 115 | because the new spelling is prettier. Benchmark only when a decision or performance
 116 | claim depends on a non-obvious empirical tradeoff.
 117 | 
 118 | An abstraction earns its place by creating a better language, carrying a law or proof,
 119 | hiding knowledge, enabling composition, translating a boundary, or localizing real
 120 | variation. One use or one implementation can be enough. Several uses or
 121 | implementations do not rescue an abstraction that gives callers nothing.
 122 | 
 123 | Dependencies are imported vocabulary and capability; dependency avoidance is not a
 124 | design objective. Do not penalize packages merely for being dependencies or treat
 125 | equivalent local machinery as free. Use or add the package that gives the best
 126 | whole-program result through notation, types, algorithms, correctness, performance,
 127 | interoperability, tooling, or maintenance. Syntax and ergonomics are sufficient benefits;
 128 | a dependency need not do something impossible to hand-write. Evaluate actual constraints
 129 | such as runtime support, bundle or build impact, licensing, privileges, and supply-chain
 130 | exposure instead of treating dependency count as quality.
 131 | 
 132 | Search the repository and ecosystem when a library or established vocabulary could
 133 | materially improve the design. During implementation, make and apply the dependency or
 134 | notation choice directly; `$dependency-recon` is the recommendation-only workflow for
 135 | requests that ask for research without edits. Multiple libraries are fine when they own
 136 | distinct concepts or compose coherently; overlap is a cost only where users must
 137 | translate between competing dialects.
 138 | 
 139 | Wrappers, facades, aliases, and re-exports may improve grammar, composition, imports,
 140 | errors, policy, or ownership. A thin layer is valuable when the use site becomes better,
 141 | even if the underlying capability remains visible.

Source: plugins/lucid-code/skills/lucid-code/references/semantic-compression.md:19–34

  19 | closure, not by the size of the mechanism in isolation.
  20 | 
  21 | ## Separate contract from accidents
  22 | 
  23 | Before preserving behavior, classify it:
  24 | 
  25 | | Kind | Treatment |
  26 | |---|---|
  27 | | Explicit requirement, external protocol, relied-on compatibility, safety or integrity property | Preserve unless the task changes it |
  28 | | Valuable but redesignable API or execution property | Preserve or improve deliberately; migrate consumers |
  29 | | Bug, incidental ordering, avoidable work, historical test artifact, or leaky implementation detail | Replace when the new design is better |
  30 | 
  31 | Treat published, documented, stable, or externally consumed surfaces as compatibility
  32 | promises unless the task authorizes migration. Internal visibility or a passing test is
  33 | not automatically a contract; undocumented behavior can still be contractual when real
  34 | consumers rely on it. Use repository evidence and task context rather than labels alone.