Adds /history [n] to display the last N user/assistant messages from
the current session (default 10, max 50).
- Tool and system messages are filtered out for readability
- Long messages are truncated to 200 characters with an ellipsis
- Multimodal content (image blocks) is collapsed to its text parts
- Invalid count argument returns a usage hint
- /history n uses prefix routing; /history uses exact routing
Also registers /history in build_help_text().
Three failure modes addressed:
1. Model reflects HEARTBEAT.md instructions back as output instead of
executing them ("HEARTBEAT.md has active tasks listed...")
2. Model narrates decision logic ("Best judgment call: stay quiet")
3. Model produces empty output for silence, runner treats it as failure,
finalization retry generates "couldn't produce a final answer" which
gets delivered to the user
Changes:
- Add _is_deliverable() pre-filter in HeartbeatService._tick() that catches
finalization fallback messages and leaked reasoning patterns before they
reach the evaluator
- Wrap Phase 2 task input with a delivery-awareness preamble telling the
model its output goes directly to the user's messaging app
- Add meta-reasoning suppression criterion to evaluator template
No changes to agent/loop.py, runner.py, providers, or config schema.
Move sessionHistoryMaxMessages, sessionHistoryMaxTokens, and
sessionFileMaxMessages out of user-facing config into internal
constants (HISTORY_MAX_MESSAGES=120, FILE_MAX_MESSAGES=2000).
- Remove 3 fields from AgentDefaults and config pipeline
- Sink enforce_file_cap into Session (was AgentLoop)
- Auto-derive token budget from context window (was configurable)
- Net -113 lines across 7 files; 723 tests green
Made-with: Cursor
The PR stores ref freshness in the metadata sidecar, so the merged main test should assert updated_at there instead of in the refs payload.
Made-with: Cursor
Resolve the MSTeams stale-reference cleanup conflict by keeping the PR's locked, atomic sidecar-meta implementation and aligning the merged test expectation locally.
Made-with: Cursor
Past assistant turns in history were prefixed with "[Message Time: ...]"
just like user turns. The model treated these as in-context demos and
started prefixing its own replies with the same marker, leaking
metadata to the user. Prompt-level warnings could not beat dozens of
prior assistant samples.
Annotate only user turns and proactive deliveries
(_channel_delivery=True, i.e. cron / heartbeat pushes whose timing is
the whole point and which are too infrequent to act as demos). Adjacent
user-side timestamps still pin every normal assistant reply for
relative-time reasoning. The now-redundant identity.md warning is
removed along with the demonstration source.
Slack inbound events with subtype=file_share were silently dropped, so
nanobot never saw messages that included attachments. Allow file_share
through, download Slack-private files using the bot token into the
local media dir, and pass them to the agent as media paths plus a
"[file: name]" / "[image: name]" placeholder in the content. Reject
responses that look like Slack's login HTML so an auth page is never
saved as if it were the user's file. Document the required files:read
scope alongside files:write so installs that read attachments are not
quietly missing the permission.
Builds on PR #3463 (commit 038a140), which introduced metadata and
session_key parameters through _LoopHook and _set_tool_context for the
cron and message tools. Three downstream gaps remained:
1. _set_tool_context's body still computes effective_key from
channel:chat_id and passes that to spawn, even when the caller
provides a thread-scoped session_key. The new parameter is wired in
for cron/message but spawn dispatch ignores it. Result: subagent
announces from threaded callers carry a channel-only
session_key_override, dropping thread_ts.
2. _process_message's system-channel branch loads the session via
key = f"{channel}:{chat_id}", ignoring msg.session_key_override.
So even when the announce InboundMessage carries the right override
(after fix 1), the consumer side discards it and routes to the
channel-level session.
3. The OutboundMessage returned from the system-channel branch has no
metadata, so slack's outbound dispatcher has no thread_ts to use and
posts the LLM's reply to the channel top-level rather than the
originating thread.
This change closes all three gaps with three small edits in loop.py.
Behavior change:
- Slack channels with reply_in_thread: true: subagent announces and
follow-up replies now arrive in the originating thread session
instead of leaking into the channel-level session.
- Other channels constructing thread-scoped session keys (matrix
threads, telegram thread mode, etc.): the session-loading and
effective-key fixes apply identically since they're platform-agnostic.
The outbound thread_ts reconstruction is slack-specific by virtue of
the session-key format slack uses; other channels would benefit from
the same pattern but are out of scope for this PR.
- Unified session mode: no change. Falls back to UNIFIED_SESSION_KEY
when session_key is not provided.
- CLI / non-channel callers: no change. They don't pass session_key
and the fallback to f"{channel}:{chat_id}" matches prior behavior.
Reproducer (slack with reply_in_thread: true):
1. From a slack thread, send a message that triggers a subagent spawn.
2. Before fix: announce lands in slack:<channel>.jsonl session,
parent agent in the thread never sees the completion event,
eventual reply (if any) posts to the channel top-level, not the
thread.
3. After fix: announce lands in slack:<channel>:<thread_ts>.jsonl,
parent agent in the thread responds within seconds, reply posts in
the thread.
When deployed with Docker and workspace mounted as a volume, sending
media files failed because relative paths (e.g. output/image.png) were
not resolved against the workspace directory. The process CWD differs
from the workspace in containerized environments, causing os.path.isfile
checks to fail in channel handlers. Normalize relative media paths at
the MessageTool entry point using get_workspace_path().
_with_thread_context prepends conversation history to the message
content. This turned "/restart" into "Slack thread context...\n\n
Current message:\n/restart", which the command router could not match
as a priority command. Skip the context enrichment when the stripped
text starts with "/".
Made-with: Cursor
cmd_restart only persisted channel + chat_id across the os.execv boundary, so
when the new process announced "Restart completed" the OutboundMessage had
no Slack thread_ts and the reply fell back to the channel root.
Serialize msg.metadata into NANOBOT_RESTART_NOTIFY_METADATA, restore it on the
RestartNotice, and forward it to OutboundMessage so the completion message
follows the same routing as the original /restart invocation.
Made-with: Cursor
The old prompt framed cron firing as a "task triggered" status report,
which led the agent to reply with things like "Done ✅ 已提醒
U0AV8BJPV8D 喝水" — exposing the user id and reading like a system log
instead of a friendly reminder. Reword it to instruct the agent to
speak directly to the user and forbid status-style language.
Made-with: Cursor
Without writing these fields into jobs.json, cron jobs created in a
Slack thread lost their thread_ts (and original session_key) after the
service was reloaded, so reminders fired into the channel root.
Made-with: Cursor
MCP resource/prompt/tool names containing spaces or special characters
(e.g. "PostgreSQL System Information") were forwarded verbatim to model
provider APIs, causing validation errors from both Anthropic and OpenAI
which require names matching ^[a-zA-Z0-9_-]{1,128}$.
Add _sanitize_name() that replaces invalid characters with underscores
and collapses consecutive underscores. Applied in MCPToolWrapper,
MCPResourceWrapper, MCPPromptWrapper constructors and the enabled_tools
filtering logic.
Closes#3468
Capture Slack thread metadata for cron and message-tool deliveries so replies stay in the originating thread, and hydrate first thread mentions with recent Slack context.
Made-with: Cursor
Include persisted turn timestamps when assembling LLM prompts so relative-date references like yesterday and today have concrete anchors.
Made-with: Cursor
The non-streaming parse path unconditionally promoted the `reasoning`
response field to `content` when content was empty. This was intended
for StepFun (whose API returns the actual answer in `reasoning`), but
it applied to every OpenAI-compatible provider — causing internal
thinking chains from models like Xiaomi MIMO to be leaked as formal
replies.
Add `reasoning_as_content: bool` to ProviderSpec (default False) and
set it only for StepFun. The fallback now requires this flag rather
than running globally.
Fixes#3443
Only mark message-tool deliveries for channel-session recording while cron jobs are running, avoiding duplicate session writes during normal user turns.
Made-with: Cursor
Route heartbeat, cron, and message-tool deliveries through one gateway helper so user-visible proactive messages are available when the channel replies.
Made-with: Cursor
When heartbeat delivers output to a channel (e.g. Telegram), the message
is a raw OutboundMessage that bypasses the channel's session. If the user
replies, their reply enters a different session with no context about the
heartbeat message, so the agent cannot follow through.
This change injects the delivered heartbeat message as an assistant turn
into the target channel's session before publishing the outbound. When
the user replies, the channel session has conversational context.
Handles unified_session mode by resolving to UNIFIED_SESSION_KEY when
enabled, matching the agent loop's own session routing.
No changes to agent/loop.py, session/manager.py, channels, providers,
or config schema — uses existing add_message() and save() APIs.
Parse the endpoint host before disabling keepalive so public hostnames that merely contain private-network substrings keep the default connection pool behavior.
Made-with: Cursor