Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/content/docs/(configuration)/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -266,14 +266,13 @@ Most config values are hot-reloaded when their files change. Spacebot watches `c
| Identity files (SOUL.md, etc.) | Yes | Next channel message renders new identity |
| Skills (SKILL.md files) | Yes | Next message / worker spawn sees new skills |
| Bindings | Yes | Next message routes using new bindings |
| Discord/Slack permissions | Yes | Next message checks new permission rules |
| Messaging adapters (Discord/Slack/Telegram/Twitch/Email/Webhook) | Yes | Adapter runtime is reconciled live; changed instances are restarted or removed without a full process restart |

### What Needs Restart

| Setting | Why |
|---------|-----|
| LLM API keys | Provider clients are initialized once (applies to `secret:`, `env:`, and literal values) |
| Messaging adapters (Discord token, webhook bind/port) | Adapter connections are long-lived |
| Agent topology (adding/removing `[[agents]]`) | Databases and event buses are per-agent |
| Database paths | Connections are opened once at startup |
| System prompts | Compiled into the binary via `include_str!` |
Expand Down
22 changes: 4 additions & 18 deletions src/api/messaging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,11 +1059,7 @@ pub(super) async fn disconnect_platform(
if platform == "twitch" {
let instance_dir = state.instance_dir.load();
if let Some(name) = adapter_name {
let safe_name: String = name
.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.collect();
let token_path = instance_dir.join(format!("twitch_token_{safe_name}.json"));
let token_path = instance_dir.join(crate::config::named_twitch_token_file_name(name));
match tokio::fs::remove_file(&token_path).await {
Ok(()) => {
tracing::info!(path = %token_path.display(), "twitch token file deleted");
Expand Down Expand Up @@ -1438,14 +1434,8 @@ pub(super) async fn toggle_platform(
"twitch",
Some(instance.name.as_str()),
);
let token_file_name = format!(
"twitch_token_{}.json",
instance
.name
.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.collect::<String>()
);
let token_file_name =
crate::config::named_twitch_token_file_name(&instance.name);
let instance_dir = state.instance_dir.load();
let token_path = instance_dir.join(token_file_name);
let perms = std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
Expand Down Expand Up @@ -2269,11 +2259,7 @@ pub(super) async fn delete_messaging_instance(
if platform == "twitch" {
let instance_dir = state.instance_dir.load();
let token_path = if let Some(name) = adapter_name {
let safe_name: String = name
.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.collect();
instance_dir.join(format!("twitch_token_{safe_name}.json"))
instance_dir.join(crate::config::named_twitch_token_file_name(name))
} else {
instance_dir.join("twitch_token.json")
};
Expand Down
2 changes: 1 addition & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub use permissions::{
pub(crate) use providers::default_provider_config;
pub use runtime::RuntimeConfig;
pub use types::*;
pub use watcher::spawn_file_watcher;
pub use watcher::{FileWatcherHandle, spawn_file_watcher};

// Re-export pub(crate) items that need crate-wide visibility.
// (GEMINI_PROVIDER_BASE_URL is only used within config submodules, no re-export needed.)
Expand Down
14 changes: 14 additions & 0 deletions src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::secrets::store::{InstancePattern, SecretField, SystemSecrets};

use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use std::collections::HashMap;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -1962,6 +1963,19 @@ pub fn binding_runtime_adapter_key(platform: &str, adapter: Option<&str>) -> Str
platform.to_string()
}

/// Build the persisted token filename for a named Twitch adapter instance.
pub fn named_twitch_token_file_name(name: &str) -> String {
let safe_name: String = name
.chars()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth bounding safe_name length to avoid path/filename length issues if someone gives a very long instance name.

Suggested change
.chars()
let safe_name: String = name
.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.take(64)
.collect();

.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.take(64)
.collect();
let hash = Sha256::digest(name.as_bytes());
let hash_prefix = hex::encode(&hash[..8]);

format!("twitch_token_{safe_name}_{hash_prefix}.json")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// Match a binding's adapter selector against an inbound message adapter.
pub(super) fn binding_adapter_matches(binding: &Binding, message: &crate::InboundMessage) -> bool {
match (&binding.adapter, message.adapter_selector()) {
Expand Down
Loading
Loading