Two display fixes based on real-world Feishu testing:
1. tool_hints.py: format_tool_hints now deduplicates by comparing the
fully formatted hint string instead of tool name alone. This fixes
`ls /Desktop` and `ls /Downloads` being incorrectly merged as
`ls /Desktop × 2`. Truly identical calls still fold correctly.
(_group_consecutive and all abbreviation logic preserved unchanged.)
2. feishu.py: inline tool hints now display one tool per line with
🔧 prefix, and use double-newline trailing to prevent Setext heading
rendering when followed by markdown `---`.
Made-with: Cursor
Tool hints should be kept as permanent content in the streaming card
so users can see which tools were called (matching the standalone card
behavior). Previously, hints were stripped when new deltas arrived or
when the stream ended, causing tool call information to disappear.
Now:
- New delta: hint becomes permanent content, delta appends after it
- New tool hint: replaces the previous hint (unchanged)
- Resuming/stream_end: hint is preserved in the final text
Updated 3 tests to verify hint preservation semantics.
Made-with: Cursor
Three fixes for inline tool hints:
1. Consecutive tool hints now replace the previous one instead of
stacking — the old suffix is stripped before appending the new one.
2. When _resuming flushes the buffer, any trailing tool hint suffix
is removed so it doesn't persist into the next streaming segment.
3. When final _stream_end closes the card, tool hint suffix is
cleaned from the text before the final card update.
Adds 3 regression tests covering all three scenarios.
Made-with: Cursor
Two improvements to Feishu streaming card experience:
1. Handle _resuming in send_delta: when a mid-turn _stream_end arrives
with resuming=True (tool call between segments), flush current text
to the card but keep the buffer alive so subsequent segments append
to the same card instead of creating a new one.
2. Inline tool hints into streaming cards: when a tool hint arrives
while a streaming card is active, append it to the card content
(e.g. "🔧 web_fetch(...)") instead of sending a separate card.
The hint is automatically stripped when the next delta arrives.
Made-with: Cursor
exec tool hints previously used val[:40] which cut paths mid-segment
(e.g. "D:\Documents\GitHub\nanobot.worktree…"). Now uses regex to
detect file paths in commands and abbreviates them properly, with
smart truncation at chain separators (&&, |, ;) as fallback.
- test_exec_head_tail_truncation: use temp script file instead of
python -c to avoid cmd.exe quote-parsing issues after PR #2893
- test_grep_files_with_matches_supports_head_limit_and_offset: query
full result set first to avoid mtime-dependent sort assumption
ExecTool hardcoded bash, breaking exec on Windows. Now uses cmd.exe
via COMSPEC on Windows with a curated minimal env (PATH, SYSTEMROOT,
etc.) that excludes secrets. bwrap sandbox gracefully skips on Windows.
* feat(dream): enhance memory cleanup with staleness detection
- Phase 1: add [FILE-REMOVE] directive and staleness patterns (14-day
threshold, completed tasks, superseded info, resolved tracking)
- Phase 2: add explicit cleanup rules, file paths section, and deletion
guidance to prevent LLM path confusion
- Inject current date and file sizes into Phase 1 context for age-aware
analysis
- Add _dream_debug() helper for observability (dream-debug.log in workspace)
- Log Phase 1 analysis output and Phase 2 tool events for debugging
Tested with glm-5-turbo: MEMORY.md reduced from 149 to 108-129 lines
across two rounds, correctly identifying and removing weather data,
detailed incident info, completed research, and stale discussions.
* refactor(dream): replace _dream_debug file logger with loguru
Remove the custom _dream_debug() helper that wrote to dream-debug.log
and use the existing loguru logger instead. Phase 1 analysis is logged
at debug level, tool events at info level — consistent with the rest
of the codebase and no extra log file to manage.
* fix(dream): make stale scan independent of conversation history
Reframe Phase 1 from a single comparison task to two independent
tasks: history diff AND proactive stale scan. The LLM was skipping
stale content that wasn't referenced in conversation history (e.g.
old triage snapshots). Now explicitly requires scanning memory files
for staleness patterns on every run.
* fix(dream): correct old_text param name and truncate debug log
- Phase 2 prompt: old_string -> old_text to match EditFileTool interface
- Phase 1 debug log: truncate analysis to 500 chars to avoid oversized lines
* refactor(dream): streamline prompts by separating concerns
Phase 1 owns all staleness judgment logic; Phase 2 is pure execution
guidance. Remove duplicated cleanup rules from Phase 2 since Phase 1
already determines what to add/remove. Fix remaining old_string -> old_text.
Total prompt size reduced ~45% (870 -> 480 tokens).
* fix(dream): add FILE-REMOVE execution guidance to Phase 2 prompt
Phase 2 was only processing [FILE] additions and ignoring [FILE-REMOVE]
deletions after the cleanup rules were removed. Add explicit mapping:
[FILE] → add content, [FILE-REMOVE] → delete content.
- Move _tool_hint implementation from loop.py to nanobot/utils/tool_hints.py
- Keep thin delegation in AgentLoop._tool_hint for backward compat
- Update test imports to test format_tool_hints directly
Made-with: Cursor
The lark-oapi client requires token types to be explicitly configured
so that the SDK can obtain and attach the tenant_access_token to raw
requests. Without this, `_fetch_bot_open_id()` would fail with
"Missing access token for authorization" because the token had not
been provisioned at the time of the call.
On Windows, certain Unicode input (emoji, mixed-script text, surrogate
pairs) causes prompt_toolkit's FileHistory to crash with
UnicodeEncodeError when writing the history file.
Fix: wrap FileHistory with a _SafeFileHistory subclass that sanitizes
surrogate characters before writing, replacing invalid sequences instead
of crashing.
Fixes#2846
Add CI step to detect unused imports (F401) and unused variables (F841)
with ruff. Clean up existing violations:
- Remove unused Consolidator import in agent/__init__.py
- Remove unused re import in agent/loop.py
- Remove unused Path import in channels/feishu.py
- Remove unused ContentRepositoryConfigError import in channels/matrix.py
- Remove unused field and CommandHandler imports in channels/telegram.py
- Remove unused exception variable in channels/weixin.py
len(content) counts Unicode code points, not UTF-8 bytes. For non-ASCII
content such as Chinese or emoji, the reported count would be lower than
the actual bytes written to disk, which is misleading to the agent.
The pydantic to_camel function generates 'e2EeEnabled' (treating 'ee'
as a word boundary) for the field 'e2ee_enabled'. Users writing
'e2eeEnabled' in their config get the default value instead.
Fix: add explicit alias='e2eeEnabled' to override the incorrect
auto-generated alias. Both 'e2eeEnabled' and 'e2ee_enabled' now work.
Fixes#2851
The _parse_tavily_usage implementation was updated to use the real
{account: {plan_usage, plan_limit, ...}} structure, but the tests
still used the old flat {used, limit, breakdown} format.
Made-with: Cursor
The Tavily /usage endpoint returns a nested "account" object with
plan_usage/plan_limit/search_usage/etc fields, not the flat structure
with used/limit/breakdown that was assumed. This caused all usage
values to be None.
The /status command tried to access web search config via
`loop.config.tools.web.search`, but AgentLoop has no `config` attribute.
This caused the search usage lookup to silently return None, so web
search provider usage was never displayed.
Fix: use `loop.web_config.search` which is the actual attribute
set during AgentLoop.__init__.
Merge the three retry-after header parsers (base, OpenAI, Anthropic)
into a single _extract_retry_after_from_headers on LLMProvider that
handles retry-after-ms, case-insensitive lookup, and HTTP date.
Remove the per-provider _parse_retry_after_headers duplicates and
their now-unused email.utils / time imports. Add test for retry-after-ms.
Made-with: Cursor
- Add nanobot-api service (OpenAI-compatible HTTP API on port 8900)
- Uses isolated workspace (/root/.nanobot/api-workspace) to avoid
session/memory conflicts with nanobot-gateway
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add byteplus and byteplus_coding_plan to thinking param providers
- Only send extra_body when reasoning_effort is explicitly set
- Use setdefault().update() to avoid clobbering existing extra_body
- Add 7 regression tests for thinking params
Made-with: Cursor
PyJWT and cryptography are optional msteams deps; they should not be
bundled into the generic dev install. Tests now skip the entire file
when the deps are missing, following the dingtalk pattern.