* fix(dream): ground commit messages and cursor advance in the real git diff
Dream consolidation could emit a /dream-log audit record that did not match
the actual file changes: build_dream_commit_message appended the LLM's
unverified resp.content, dream_run_completed only checked the stop reason, and
file contents were deliberately omitted from the prompt. The combination let a
single-turn self-report become the durable audit record.
- gitstore: add summarize_working_tree() — a structured, machine-derived
summary (per-file +N/-M, totals, capped unified diff) of working-tree
changes vs HEAD. Pure filesystem/git ground truth, never LLM narrative.
- memory: build_dream_commit_message now takes the diff body instead of resp;
dream_content_diff() exposes the real delta over SOUL/USER/MEMORY.md only
(excludes .dream_cursor so cursor writes aren't mistaken for edits);
build_dream_prompt embeds current file contents so the model edits reality,
not a stale mental model.
- builtin/cli: both Dream paths now compute the diff, gate cursor advance on
a non-empty delta (no-op runs no longer swallow history), and commit with
the diff-grounded message. Non-git workspaces fall back to the completion
check.
- dream.md: document that contents are embedded, and add a chain-of-
verification guardrail so the model's summary cannot claim unmade edits.
A regression test proves a lying resp.content never reaches the audit log
while the real diff does.
* fix(dream): mark non-UTF-8 memory files as binary in diff summary
Address review feedback (Q1 on PR #4673): summarize_working_tree read
working-tree files with errors="replace", which would emit U+FFFD
replacement chars into the audit record if a memory file ever held
invalid UTF-8 — misrepresenting the diff it is meant to make truthful.
Switch to errors="strict" and catch UnicodeDecodeError: a non-UTF-8
(or binary/corrupt) file is now recorded as "{path}: binary or
non-UTF-8 file changed" and omitted from the unified diff, so the
audit record stays honest. An empty diff block is also suppressed when
all changes are binary.
Adds a defensive regression test asserting no replacement char leaks.
maintainer edit: update the existing transient retry tests for reconnect-first behavior and keep structured retry-failure coverage in the focused MCP transient suite.
maintainer edit: cron is also a trigger source, so keep the new CLI-delivered source explicitly named as local trigger across backend, WebUI, docs, and tests.
maintainer edit: remove the _last_compacted_at maintenance state, gate idle compaction on whether a session still has a removable tail, and sort WebUI sessions by the latest visible transcript activity.
Replace the tuple(list[dict], int) return of
Session.retain_recent_legal_suffix with a named RetentionResult
dataclass that exposes retained, dropped,
already_consolidated_count, and new_last_consolidated fields.
The tuple return was easy to misuse because the second value only
made sense relative to the first and the old last_consolidated
cursor. The named fields make the archive-skip semantics explicit
at every call site.
No behavior change. All existing tests pass unchanged in semantics.
Refs #4136
Signed-off-by: axelray-dev <110029405+axelray-dev@users.noreply.github.com>
Treat max_messages as a last-resort replay guard now that consolidation and idle auto-compact own normal history reduction. Raising the default avoids frequent sliding-window prefix churn in moderate conversations without adding a new cache-policy knob.
Combine malformed tool-call handling with placeholder filtering and a
no-tools fallback so a relay that returns tool_use blocks with null
id/name/input can no longer crash a turn or permanently wedge a session.
Adapted to the ContextGovernor architecture (context governance now lives
in nanobot/agent/context_governance.py, not runner.py):
- ToolCallRequest.has_valid_name(): single source of truth for "usable
name" (non-empty string).
- tool_hints.format_tool_hints(): skip tool calls with a non-string/empty
name instead of raising AttributeError on the whole turn.
- ContextGovernor.strip_placeholder_assistant_messages() and
strip_malformed_tool_calls() (plus the _tool_call_name_is_valid helper):
history-cleaning staticmethods invoked at the START of
prepare_for_model() — strip_placeholder, then strip_malformed, then the
existing drop_orphan/backfill chain. Both only repair the model-facing
copy and leave persisted history untouched (return a copy, or the same
list when nothing changes). Also wired into runner's minimal-repair path.
- AgentRunner._drop_malformed_tool_calls(): returns
(dropped, all_dropped, original_finish_reason); clears finish_reason to
"stop" when all calls are dropped.
- AgentRunner._malformed_tool_call_retry_messages() + _request_model
malformed_retry flag: when an all-dropped tool_calls response comes back,
retry once with a corrective note; if the retry STILL comes back
all-dropped, fall back to _request_no_tools for graceful text degradation.
Tests for the history-cleaning methods live with ContextGovernor in
tests/agent/test_runner_governance.py; response-layer and tool-hint tests
stay on AgentRunner / tool_hints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract model-facing context governance from AgentRunner.
Only compact in-flight tool results when the model request is over budget, keep compacted IDs stable within a turn, and allow the newest result to be compacted as a last resort when it is the remaining source of overflow.
Add fail_on_tool_error to AgentDefaults and wire it through
AgentLoop -> SubagentManager -> AgentRunSpec.
Previously hardcoded to True in SubagentManager._run_subagent.
Now configurable via config.json with default True for backward
compatibility. When set to False, subagents can retry on minor
tool errors instead of immediately failing.
Changes:
- nanobot/config/schema.py: add fail_on_tool_error field (default True)
- nanobot/agent/subagent.py: accept and forward fail_on_tool_error
- nanobot/agent/loop.py: pass config through to SubagentManager
- tests/agent/test_subagent.py: add regression test
Signed-off-by: axelray-dev <110029405+axelray-dev@users.noreply.github.com>
When dream.enabled is false, the Dream cron job never runs, so the
dream cursor (.dream_cursor) stays at its initial value (0). This
causes read_recent_history_for_prompt() to treat every history entry
as unprocessed, injecting the full chat history into every system
prompt and growing without bound.
Fix: fast-forward the dream cursor to the latest history entry at
gateway startup when Dream is disabled.