* refactor(dream): replace two-phase Dream class with simple cron + process_direct
- Remove the heavyweight Dream class (AgentRunner-based two-phase system)
from nanobot/agent/memory.py
- Delete dream_phase1.md and dream_phase2.md templates
- New dream.md template serves as the consolidation prompt
- Cron callback uses agent.process_direct(prompt, session_key=\"dream\")
instead of agent.dream.run()
- Always performs git auto_commit after execution
- /dream command updated to use process_direct + git commit
- DreamConfig kept for backward compatibility; deprecated fields
(model_override, max_batch_size, max_iterations, annotate_line_ages)
are ignored but accepted in config
- interval_h remains configurable via agents.defaults.dream.interval_h
- Update tests and webui settings to match new architecture
* feat(loop): add ephemeral mode to process_direct, skip history writes for Dream
When ephemeral=True, _state_save skips enforce_file_cap (which calls
raw_archive -> append_history) and consolidator.maybe_consolidate_by_tokens.
This prevents Dream sessions from creating a positive feedback loop where
they process their own output. The session IS still saved to disk.
* fix(loop): skip extra hooks for ephemeral sessions (Dream)
* feat(dream): per-run timestamped sessions with rotation for WebUI
* test(config): restore DreamConfig schedule and alias tests
* fix(dream): include LLM response summary in git auto-commit message
The old two-phase Dream class included the Phase 1 analysis in the git
commit message body. The new single-phase version lost this. Restore it
by extracting resp.content from the process_direct return value and
appending it to the commit message in both the cron handler and the
/dream command.
* fix(test): accept ephemeral kwarg in test_openai_api fake_process
* refactor(dream): merge dream_session.py into MemoryStore
The standalone dream_session.py module only contained three small helpers
that all revolve around MemoryStore concerns (session keys, commit messages,
file pruning). Fold them into MemoryStore as @staticmethod to reduce
indirection and avoid a 35-line module with no independent reason to exist.
* fix(test): address code review — patch correct instance, use actual function
- Fix test_ephemeral_skips_raw_archive to patch loop.context.memory
instead of the fixture's separate MemoryStore instance
- Fix TestDreamCommitMessage to call MemoryStore.build_dream_commit_message
instead of reimplementing the logic inline
- Move Dream helpers in memory.py above the Consolidator section comment
to avoid misleading visual boundary
* fix(dream): gate cursor advancement and restrict tools
maintainer edit: Dream now processes backlog from the oldest unprocessed entries, only advances the cursor after a completed ephemeral run, and uses a restricted file-only tool registry for background consolidation.
* fix(dream): skip idle compact for dream sessions
Dream runs use internal dream:* sessions that are pruned by Dream retention. Exclude them from AutoCompact scheduling, archive execution, and summary injection so idle-session compaction cannot truncate Dream transcripts.
* fix(dream): keep batched history isolated
* feat(dream): tag archived memory for single-phase Dream
---------
Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
The previous fix made retain_recent_legal_suffix return the actual dropped
message list, but already_consolidated was still computed with
min(before_last_consolidated, len(dropped)), which assumes dropped messages
are always a prefix. In the else branch (tail has no user messages), dropped
may include messages from after the consolidated prefix, causing
already_consolidated to skip too many and leaving tail messages neither
retained nor raw-archived.
Fix by having retain_recent_legal_suffix return (dropped,
already_consolidated_count) where already_consolidated_count is computed
against original message indices. Also fix last_consolidated update to count
how many retained messages were inside the old consolidated prefix.
When retain_recent_legal_suffix hits the else branch (tail has no user
messages), it takes a non-contiguous slice from the middle of the session.
enforce_file_cap incorrectly assumed dropped messages were always a prefix
(before[:dropped_count]), causing user messages to be both archived and
retained, and some messages to silently disappear.
Fix by having retain_recent_legal_suffix return the actual dropped message
list using identity-based diff, so enforce_file_cap no longer needs to
guess which messages were removed.
Extract is_image_file() and reference_non_image_attachments() from
AgentLoop private static methods into nanobot/utils/document.py where
they belong alongside extract_documents(). Simplify config lookup by
removing dead isinstance(dict) branch.
Replace asyncio.sleep(0.05) with an asyncio.Event + patched Lock.acquire
to guarantee the waiting task has reached the lock before asserting. Add
a test confirming LongTaskTool and CompleteGoalTool ContextVars are
isolated, and document the design intent in _GoalToolsMixin.
Remove standalone nanobot/heartbeat/ service and replace it with an
auto-registered system cron job on gateway startup. Key behaviors preserved:
- HeartbeatConfig (enabled, interval_s, keep_recent_messages) remains in
GatewayConfig for backward compatibility.
- On startup, if enabled, a system cron job "heartbeat" is registered with
schedule derived from interval_s.
- HEARTBEAT.md is checked on each tick; empty/template-identical files skip
to avoid wasting LLM calls.
- Post-run evaluate_response and session history truncation
(keep_recent_messages) are retained.
- Delivery target selection, deliverable filtering, and preamble guidance
are preserved.
Files removed:
- nanobot/heartbeat/__init__.py
- nanobot/heartbeat/service.py
- tests/heartbeat/*
- tests/agent/test_heartbeat_service.py
Templates and docs updated to reflect cron-based usage.
`long_task` registers a sustained objective, but `AgentRunner` would
still exit with `stop_reason="completed"` when the LLM produced a final
text response without calling `complete_goal`. This defeated the purpose
of sustained goals.
Add `goal_active_predicate` and `goal_continue_message` to `AgentRunSpec`.
When the predicate returns `True` at the natural completion checkpoint,
inject a continuation message via the existing `_try_drain_injections`
machinery, forcing the runner to continue looping.
Also extract the default continuation prompt to
`nanobot/utils/runtime.py` alongside the existing recovery-message
builders.
- Remove generated_image_paths_from_messages() and _extract_text_payload() from artifacts.py (no runtime callers)
- Remove session_attachments.py entirely (merge_turn_media_into_last_assistant and stage_media_paths_for_session_replay had no runtime callers)
- Remove test_session_media_persist.py and the orphaned test in test_artifacts.py
The runtime media-attachment mechanism was broken for streaming channels
(e.g. WebSocket): the _streamed flag caused _send_once to skip the final
OutboundMessage that carried generated media, so images were never delivered.
Rather than adding complex coordination between streaming and media delivery,
delegate image delivery to the LLM: after generate_image returns artifact
paths, the next_step prompt now instructs the LLM to call the message tool
with the paths in the media parameter. This works uniformly across all
channels, streaming or not.
Remove generated_media from TurnContext, _assemble_outbound, and _state_save.
Update prompts in identity.md, SKILL.md, message tool description, and
artifacts.py to reflect the new flow.
Adds ImageGenerationProvider ABC with shared __init__, _http_post(), and
_require_images(). Introduces _IMAGE_GEN_PROVIDERS registry with
register/get/image_gen_provider_configs() helpers.
Four existing providers (OpenRouter, AIHubMix, Gemini, MiniMax) now inherit
from the base class and self-register. Adding a new provider only requires
writing one class + one registration line.
Eliminates if/else chains in the tool dispatch and hardcoded provider config
dicts in commands.py (3 sites) and nanobot.py (1 site). Fixes the agent CLI
command missing image_generation_provider_configs entirely.
Also simplifies test monkeypatch targets to patch the registry lookup.
Extract the [M] Model Presets interactive CRUD screen from PR #3696
and adapt it to the current main branch schema (fallback_models
instead of fallback_presets). Adds preset cache, field handlers for
model_preset/provider/fallback_models, and 9 new tests.
Replace AutoCompact._archive() direct session mutation with delegation
to Consolidator.compact_idle_session(). Remove _split_unconsolidated()
method since that logic now lives inside compact_idle_session.
All session mutation for idle compaction now goes through the
Consolidator's lock, eliminating the race condition between
background token consolidation and idle TTL compaction.
Changes:
- autocompact.py: rewrite _archive() to call compact_idle_session,
remove _split_unconsolidated(), clean up unused imports
- test_autocompact_unit.py: replace TestArchive/TestSplitUnconsolidated
with TestArchiveDelegates that verifies delegation behavior
- test_auto_compact.py: convert all consolidator.archive mocks to
consolidator.compact_idle_session mocks via _make_fake_compact helper
When background consolidation runs with a stale session reference (captured
before AutoCompact replaced the session via compact_idle_session), it could
operate on outdated data. Now, after acquiring the per-session lock, the
method refreshes its session reference from SessionManager.get_or_create().
If the session was replaced, it swaps in the fresh reference before doing
any consolidation work.
This prevents a race where AutoCompact truncates an idle session while a
background maybe_consolidate_by_tokens call is in flight with the old
session object.
Add Consolidator.compact_idle_session(session_key, max_suffix=8) that
performs hard-truncation of idle sessions under the per-session
consolidation lock. This is the single lock-protected path for AutoCompact
to use instead of modifying session state directly, fixing the race
condition between AutoCompact and Consolidator.
Behavior:
- Acquires per-session consolidation lock
- Invalidates cache and reloads fresh from disk
- Splits unconsolidated tail into archive prefix and retained suffix
- Archives prefix via LLM (with raw_archive fallback on failure)
- Persists _last_summary in session metadata on success
- Returns summary text, None on LLM failure, or '' if nothing to archive
Tests: 6 new tests covering prefix archival, empty session timestamp
refresh, (nothing) summary exclusion, LLM failure fallback,
last_consolidated offset, and lock acquisition verification.
Centralize runner_wall_llm_timeout_s in session goal_state metadata helpers so
spawned subagents inherit the same policy as AgentLoop without coupling to
long_task. Pass optional resolver into SubagentManager and add tests.
Co-authored-by: Cursor <cursoragent@cursor.com>