fix(discord): track gateway sequence number and handle reconnect opcodes (#159)

* feat(providers): add provider-aware API key resolution

- Add resolve_api_key() function that checks provider-specific env vars first
- For Anthropic, checks ANTHROPIC_OAUTH_TOKEN before ANTHROPIC_API_KEY
- Falls back to generic ZEROCLAW_API_KEY and API_KEY env vars
- Update create_provider() to use resolved_key instead of raw api_key
- Trim and filter empty strings from input keys

This enables setup-token support for Anthropic by checking ANTHROPIC_OAUTH_TOKEN
before ANTHROPIC_API_KEY when resolving credentials.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(providers): add Anthropic setup-token support

- Rename api_key field to credential for clarity
- Add is_setup_token() method to detect setup-token format (sk-ant-oat01-)
- Add input trimming and empty string filtering
- Use Bearer auth for setup-tokens, x-api-key for regular API keys
- Update error message to mention both ANTHROPIC_API_KEY and ANTHROPIC_OAUTH_TOKEN
- Add test for setup-token detection
- Add test for whitespace trimming in new()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip serialization of config_path and workspace_dir to prevent save() failures

The config_path and workspace_dir fields are computed paths that should not be
serialized to the config file. When loading from TOML, these fields would be
deserialized as empty paths (or stale paths), causing save() to fail with
"Failed to write config file".

Fixes #112

Changes:
- Add #[serde(skip)] to config_path and workspace_dir fields
- Set computed paths in load_or_init() after deserializing from TOML

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(discord): track gateway sequence number and handle reconnect opcodes

Three Discord Gateway issues fixed:

1. **Heartbeat sent `null` sequence** — Per Discord docs, the Gateway may
   disconnect bots that don't include the last sequence number in heartbeats.
   Now tracked via `sequence: i64` and included in every heartbeat.

2. **Dispatch sequence ignored** — The `s` field from dispatch events was
   never stored. Now extracted and tracked from every event.

3. **Opcodes 7/9 silently ignored** — Reconnect (op 7) and Invalid Session
   (op 9) caused the bot to hang on a dead connection. Now breaks the event
   loop so the daemon supervisor can restart the channel cleanly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(memory): use SHA-256 for embedding cache keys instead of DefaultHasher

- Replace DefaultHasher with SHA-256 for deterministic cache keys
- DefaultHasher is explicitly documented as unstable across Rust versions
- Truncate SHA-256 to 8 bytes (16 hex chars) to match previous format
- Ensures embedding cache is deterministic across Rust compiler versions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Argenis 2026-02-15 10:25:38 -05:00 committed by GitHub
parent 722c99604c
commit a5241f34ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 180 additions and 12 deletions

View file

@ -9,7 +9,11 @@ use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Workspace directory - computed from home, not serialized
#[serde(skip)]
pub workspace_dir: PathBuf,
/// Path to config.toml - computed from home, not serialized
#[serde(skip)]
pub config_path: PathBuf,
pub api_key: Option<String>,
pub default_provider: Option<String>,
@ -694,11 +698,16 @@ impl Config {
if config_path.exists() {
let contents =
fs::read_to_string(&config_path).context("Failed to read config file")?;
let config: Config =
let mut config: Config =
toml::from_str(&contents).context("Failed to parse config file")?;
// Set computed paths that are skipped during serialization
config.config_path = config_path.clone();
config.workspace_dir = zeroclaw_dir.join("workspace");
Ok(config)
} else {
let config = Config::default();
let mut config = Config::default();
config.config_path = config_path.clone();
config.workspace_dir = zeroclaw_dir.join("workspace");
config.save()?;
Ok(config)
}