refactor(dream): replace two-phase Dream class with simple cron + process_direct (#3990)

* 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>
This commit is contained in:
chengyongru
2026-06-02 22:46:47 +08:00
committed by GitHub
co-authored by Xubin Ren
parent b2ae5d936f
commit d1a94dae8a
31 changed files with 939 additions and 807 deletions
+11 -15
View File
@@ -82,38 +82,37 @@ class TestResolveConfig:
assert saved["channels"]["telegram"]["token"] == "${MY_TOKEN}"
def test_preserves_excluded_fields_when_no_env_refs(self, tmp_path):
"""Regression: fields with ``exclude=True`` (e.g. DreamConfig.cron)
"""Regression: fields with ``exclude=True`` (e.g. ProviderConfig.openai_codex)
must survive ``resolve_config_env_vars`` when the config has no
``${VAR}`` references. Previously the unconditional dump→revalidate
roundtrip silently dropped them."""
config_path = tmp_path / "config.json"
config_path.write_text(
json.dumps(
{"agents": {"defaults": {"dream": {"cron": "5 11 * * *"}}}}
{"providers": {"openaiCodex": {"apiKey": "secret"}}}
),
encoding="utf-8",
)
raw = load_config(config_path)
assert raw.agents.defaults.dream.cron == "5 11 * * *"
assert raw.providers.openai_codex.api_key == "secret"
resolved = resolve_config_env_vars(raw)
assert resolved.agents.defaults.dream.cron == "5 11 * * *"
assert resolved.agents.defaults.dream.describe_schedule() == (
"cron 5 11 * * * (legacy)"
)
assert resolved.providers.openai_codex.api_key == "secret"
def test_preserves_excluded_fields_with_env_refs(self, tmp_path, monkeypatch):
"""Excluded fields must also survive when the config contains
``${VAR}`` refs elsewhere. An in-place walk preserves the legacy
``cron`` override even as unrelated string fields are substituted."""
``${VAR}`` refs elsewhere. An in-place walk preserves the excluded
field even as unrelated string fields are substituted."""
monkeypatch.setenv("TEST_API_KEY", "resolved-key")
config_path = tmp_path / "config.json"
config_path.write_text(
json.dumps(
{
"agents": {"defaults": {"dream": {"cron": "5 11 * * *"}}},
"providers": {"groq": {"apiKey": "${TEST_API_KEY}"}},
"providers": {
"openaiCodex": {"apiKey": "secret"},
"groq": {"apiKey": "${TEST_API_KEY}"},
}
}
),
encoding="utf-8",
@@ -123,7 +122,4 @@ class TestResolveConfig:
resolved = resolve_config_env_vars(raw)
assert resolved.providers.groq.api_key == "resolved-key"
assert resolved.agents.defaults.dream.cron == "5 11 * * *"
assert resolved.agents.defaults.dream.describe_schedule() == (
"cron 5 11 * * * (legacy)"
)
assert resolved.providers.openai_codex.api_key == "secret"