- Add model_fallbacks and api_keys to ReliabilityConfig
- Implement per-model fallback chain in ReliableProvider
- Add API key rotation on auth failures (401/403)
- Add retry-after header parsing and exponential backoff
- Integrate failover into chat_with_system and chat_with_history
- 20 unit tests covering failover, rotation, and retry logic
- Switch Provider trait methods to return structured ChatResponse
- Map OpenAI-compatible tool_calls into shared ToolCall type
- Update reliable/router wrappers and provider tests for new interface
- Make agent loop prefer structured tool calls with text fallback parsing
- Adapt gateway replies to structured responses with safe tool-call fallback
Fixes#221 - SQLite Memory Override bug.
This PR resolves memory overwrite behavior in autosave paths by replacing fixed memory keys with unique keys, and improves short-horizon recall quality in channel runtime.
**Root Cause**
SQLite memory uses a unique constraint on `memories.key` and writes with `ON CONFLICT(key) DO UPDATE`.
Several autosave paths reused fixed keys (or sender-stable keys), so newer messages overwrote earlier conversation entries.
**Changes**
- Channel runtime: autosave key changed from `channel_sender` to `channel_sender_messageId`
- Added memory-context injection before provider calls (aligned with agent loop behavior)
- Agent loop: autosave keys changed from fixed `user_msg`/`assistant_resp` to UUID-suffixed keys
- Gateway: Webhook/WhatsApp autosave keys changed to UUID-suffixed keys
All CI checks passing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add multi-turn conversation history and tool execution
Major enhancement to the agent loop:
**Multi-turn conversation:**
- Add `ChatMessage` type with system/user/assistant constructors
- Add `chat_with_history` method to Provider trait (default impl
delegates to `chat_with_system` for backward compatibility)
- Implement native `chat_with_history` on OpenRouter, Compatible,
Reliable, and Router providers to send full message history
- Interactive mode now maintains persistent history across turns
**Tool execution:**
- Agent loop now parses `<tool_call>` XML tags from LLM responses
- Executes tools from the registry and feeds results back as
`<tool_result>` messages
- Agentic loop continues until LLM produces final text (no tool calls)
- MAX_TOOL_ITERATIONS (10) safety limit prevents runaway loops
- System prompt includes structured tool-use protocol with JSON schemas
**Types:**
- `ChatMessage`, `ChatResponse`, `ToolCall`, `ToolResultMessage`,
`ConversationMessage` — full conversation modeling types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review comments on multi-turn + tool execution
- Add history sliding window (MAX_HISTORY_MESSAGES=50) to prevent
unbounded conversation history growth in interactive mode
- Add 404→Responses API fallback in compatible.rs chat_with_history,
matching chat_with_system behavior
- Use super::api_error() for error sanitization in compatible.rs
instead of raw error body (prevents secret leakage)
- Add missing operational logs in reliable.rs chat_with_history:
recovery, non-retryable, fallback switch warnings
- Add trim_history tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address second round of review comments
- Sanitize raw error text in compatible.rs chat_with_system using
sanitize_api_error (prevents leaking secrets in error messages)
- Add chat_with_history to MockProvider in reliable.rs tests so
the retry/fallback path is exercised end-to-end
- Add chat_with_history_retries_then_recovers and
chat_with_history_falls_back tests
- Log warning on malformed <tool_call> JSON instead of silent drop
- Flush stdout after print! in agent_turn so output appears before
tool execution on line-buffered terminals
- Make interactive mode resilient to transient errors (continue
loop instead of terminating session)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(providers): use Bearer auth for Gemini CLI OAuth tokens
When credentials come from ~/.gemini/oauth_creds.json (Gemini CLI),
send them as Authorization: Bearer header instead of ?key= query
parameter. API keys from env vars or config continue using ?key=.
Fixes#194
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(gemini): harden OAuth bearer auth flow and tests
* fix(gemini): granular auth source tracking and review fixes
Build on chumyin's auth model refactor with:
- Expand GeminiAuth to 4 variants (ExplicitKey/EnvGeminiKey/EnvGoogleKey/
OAuthToken) so auth_source() uses stored discriminant without re-reading
env vars at call time
- Add is_api_key()/credential() helpers on the enum
- Upgrade expired OAuth token log from debug to warn
- Add tests: provider_rejects_empty_key, auth_source_explicit_key,
auth_source_none_without_credentials
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: apply rustfmt to fix CI lint failures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: root <root@instance-20220913-1738.vcn09131738.oraclevcn.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
Skip retries on non-retryable HTTP client errors (4xx) to avoid wasting time on requests that will never succeed.
- Added is_non_retryable() function to detect non-retryable errors
- 4xx client errors (400, 401, 403, 404) are now non-retryable
- Exceptions: 429 (rate limiting) and 408 (timeout) remain retryable
- 5xx server errors remain retryable
- Fallback logic now skips retries for non-retryable errors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Warn when fallback providers share the same API key as primary (could fail
if providers require different keys)
- Warm up all providers instead of just the first, continuing on warmup failures
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The first API request after daemon startup consistently timed out (120s)
when using channels (Telegram, Discord, etc.), requiring a retry before
succeeding. This happened because the reqwest HTTP client's connection
pool was cold — no TLS handshake, DNS resolution, or HTTP/2 negotiation
had occurred yet.
The fix adds a `warmup()` method to the Provider trait that establishes
the connection pool on startup by hitting a lightweight endpoint
(`/api/v1/auth/key` for OpenRouter). The channel server calls this
immediately after creating the provider, before entering the message
processing loop.
Tested on Raspberry Pi 5 (aarch64) with OpenRouter + DeepSeek v3.2 via
Telegram channel. Before: first message took 2-7 minutes (120s timeout +
retries). After: first message responds in <30s with no retries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Expand communication style presets (professional, expressive, custom)
- Enrich SOUL.md with human-like tone and emoji-awareness guidance
- Add crash recovery and sub-task scoping guidance to AGENTS.md scaffold
- Add 'Use when / Don't use when' guidance to TOOLS.md and runtime prompts
- Implement memory hygiene system with configurable archiving and retention
- Add MemoryConfig options: hygiene_enabled, archive_after_days, purge_after_days, conversation_retention_days
- Archive old daily memory and session files to archive subdirectories
- Purge old archives and prune stale SQLite conversation rows
- Add comprehensive tests for new features