- Add MAX_CONFIG_SIZE constant (1 MiB) to limit single config file size
- Check file size via os.stat() before reading into memory
- Oversized files raise ConfigError with descriptive message instead of
being fully read (which could cause OOM with multi-GB files)
- Use single os.stat() call to get both mtime and size atomically
- Update README: document size limit in security mechanisms and notes
- Existing small configs (ini/json/yaml, typically <10KB) are unaffected
- Add configurable recv_timeout (default 10s) to MsgServer
- _handle_client now uses recv(s, timeout=...) instead of infinite blocking recv(s)
- Catch TimeoutError and close the slow client connection
- This prevents a single stalled connection from blocking the entire main select loop
- Update README with recv_timeout parameter and Slowloris mitigation docs
- Backward compatible: keyword-only arg with sensible default
- Fix all references from main.py to aibox.py (actual entry file)
- Update installation to use pip install -r requirements.txt
- Expand WebSocket message types table: add line/task_error/task_added/
notification/connection_ack/interrupt_ack, correct chunk/complete/error semantics
- Fix message flow example: tool calls are reported via 'line' messages,
not a non-existent 'tool_call' type
- Document _sanitize_tool_result() and error recovery (_rollback_messages)
in development guide
- Add FAQ entry for tool-call-induced API failures
- Add v2.1 changelog covering all robustness fixes (reasoning_content
filtering, tool_call id fallback, control character sanitization,
message rollback on API failure)
Root causes of API request failures after tool calls:
1. reasoning_content (an API response field) was echoed back in assistant
messages, which could cause API format errors
2. tool_call id could be empty string when model didn't return one in a
streaming chunk, producing invalid tool_calls that caused 400 errors
3. Tool execute() could return None or non-string types; direct string
operations (len(), + "\n", [:200]) raised TypeError and produced
non-string content in tool messages, breaking JSON serialization
4. Tool output (e.g. from run_command) could contain control characters
(NULL/BEL/BS) that corrupted JSON payloads
5. When API request failed (network/4xx/5xx/parse error), messages added
in that turn (user msg, assistant msg with tool_calls, tool responses)
remained in self.messages, permanently poisoning conversation history
so all subsequent requests would also fail
Fixes:
- Stop sending reasoning_content back to API in message construction;
normalize assistant message content: null when tool_calls present, ""
when neither content nor tool_calls exist, str otherwise
- Sanitize tool messages: ensure tool_call_id is non-empty (fallback id)
and content is always a string
- Generate fallback tool_call id when model omits it; handle streaming id
delivery where id arrives in a later chunk
- Add _sanitize_tool_result(): coerce None/non-string to str, strip
control characters (preserve \n \t \r) to prevent JSON/API corruption
- Wrap execute_tool call in try/except in _handle_tool_calls for double
safety; ensure history logging never crashes
- Truncate oversized results with an explicit note instead of replacing
with a generic error that lost all useful output
- Add _rollback_messages(to_length) and record msgs_before_turn at start
of think_and_respond; on any exception (RequestException, JSONDecodeError,
or unexpected Exception), rollback self.messages to the pre-turn state
so the conversation is not left in a broken state
- In _handle_tool_calls, wrap execute_tool in try/except and always
produce a safe string result
Root cause: aibox.py think_and_respond recursively calls itself after tool
execution, and each recursive call's complete callback carries only the
final-round full_response. The old finish_ai logic treated complete as a
full-text alignment signal — when full_text != self._ai_text (which always
happened after recursive tool calls because _ai_text accumulated all chunks
while complete only held the last round), it triggered remove+remount,
causing visible duplicate/flash of AI responses. A secondary issue was
dialogue_ended arriving before complete, which made a second finish_ai call
remount the message a second time.
Changes:
- Remove the full-text replacement branch in finish_ai; chunk streaming is
now the authoritative content source, complete is a finalization signal only
- Add _ai_finalized flag: start_ai resets it, finish_ai sets it; repeated
finish_ai calls (dialogue_ended + complete) return early to prevent remount
- append_ai drops late chunks after finalization to avoid dirty appends
- Empty pre-built widget without chunks still gets filled via w.update(full_text)
- Full lost-frame fallback (no widget + full_text) is preserved
- Update BotOps/README.md protocol section to reflect new complete semantics
- New token_manager.js: silent refresh timer (80% lifetime) + global
fetch 401 interceptor with one-shot retry.
- dnfs.py /whoami now returns iat, created_at, keep_time for expiry
estimation.
- Dnfs.html loads token_manager.js before dnfs.js (zero change to dnfs.js).
- Graceful fallback: stale tokens without new fields, refresh failures,
and /refresh_token 401 itself all degrade to the existing 401→login flow.
- POST /refresh_token: accept current Bearer token (or Cookie fallback),
verify RSA signature, check grace window (iat + keep_time * multiplier)
and refresh count limit, then issue a new token.
- Token payload extended with iat, refresh_count, max_refresh fields.
- iat preserves original issue time; created_at updates on each refresh.
- Rate limited: 5 req / 5 min / IP via RateLimiter.
- Config: Token.refresh_grace_multiplier (default 2), Token.max_refresh (default 10).
- Backward compatible: old tokens without new fields still pass proxy checks.