refactor: remove deprecated memory_window, harden wizard display

This commit is contained in:
Xubin Ren
2026-03-20 09:44:06 +00:00
committed by Xubin Ren
parent c3a4b16e76
commit f44c4f9e3c
6 changed files with 44 additions and 74 deletions

View File

@@ -322,9 +322,6 @@ def onboard(
console.print(f"[red]✗[/red] Error during configuration: {e}") console.print(f"[red]✗[/red] Error during configuration: {e}")
console.print("[yellow]Please run 'nanobot onboard' again to complete setup.[/yellow]") console.print("[yellow]Please run 'nanobot onboard' again to complete setup.[/yellow]")
raise typer.Exit(1) raise typer.Exit(1)
else:
console.print("[dim]Config template now uses `maxTokens` + `contextWindowTokens`; `memoryWindow` is no longer a runtime setting.[/dim]")
_onboard_plugins(config_path) _onboard_plugins(config_path)
# Create workspace, preferring the configured workspace path. # Create workspace, preferring the configured workspace path.
@@ -464,21 +461,30 @@ def _load_runtime_config(config: str | None = None, workspace: str | None = None
console.print(f"[dim]Using config: {config_path}[/dim]") console.print(f"[dim]Using config: {config_path}[/dim]")
loaded = load_config(config_path) loaded = load_config(config_path)
_warn_deprecated_config_keys(config_path)
if workspace: if workspace:
loaded.agents.defaults.workspace = workspace loaded.agents.defaults.workspace = workspace
return loaded return loaded
def _print_deprecated_memory_window_notice(config: Config) -> None: def _warn_deprecated_config_keys(config_path: Path | None) -> None:
"""Warn when running with old memoryWindow-only config.""" """Hint users to remove obsolete keys from their config file."""
if config.agents.defaults.should_warn_deprecated_memory_window: import json
from nanobot.config.loader import get_config_path
path = config_path or get_config_path()
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return
if "memoryWindow" in raw.get("agents", {}).get("defaults", {}):
console.print( console.print(
"[yellow]Hint:[/yellow] Detected deprecated `memoryWindow` without " "[dim]Hint: `memoryWindow` in your config is no longer used "
"`contextWindowTokens`. `memoryWindow` is ignored; run " "and can be safely removed.[/dim]"
"[cyan]nanobot onboard[/cyan] to refresh your config template."
) )
# ============================================================================ # ============================================================================
# Gateway / Server # Gateway / Server
# ============================================================================ # ============================================================================
@@ -506,7 +512,6 @@ def gateway(
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
config = _load_runtime_config(config, workspace) config = _load_runtime_config(config, workspace)
_print_deprecated_memory_window_notice(config)
port = port if port is not None else config.gateway.port port = port if port is not None else config.gateway.port
console.print(f"{__logo__} Starting nanobot gateway version {__version__} on port {port}...") console.print(f"{__logo__} Starting nanobot gateway version {__version__} on port {port}...")
@@ -697,7 +702,6 @@ def agent(
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
config = _load_runtime_config(config, workspace) config = _load_runtime_config(config, workspace)
_print_deprecated_memory_window_notice(config)
sync_workspace_templates(config.workspace_path) sync_workspace_templates(config.workspace_path)
bus = MessageBus() bus = MessageBus()

View File

@@ -247,12 +247,20 @@ def _mask_value(value: str) -> str:
def _format_value(value: Any, rich: bool = True, field_name: str = "") -> str: def _format_value(value: Any, rich: bool = True, field_name: str = "") -> str:
"""Format a value for display, masking sensitive fields.""" """Single recursive entry point for safe value display. Handles any depth."""
if value is None or value == "" or value == {} or value == []: if value is None or value == "" or value == {} or value == []:
return "[dim]not set[/dim]" if rich else "[not set]" return "[dim]not set[/dim]" if rich else "[not set]"
if field_name and _is_sensitive_field(field_name) and isinstance(value, str): if _is_sensitive_field(field_name) and isinstance(value, str):
masked = _mask_value(value) masked = _mask_value(value)
return f"[dim]{masked}[/dim]" if rich else masked return f"[dim]{masked}[/dim]" if rich else masked
if isinstance(value, BaseModel):
parts = []
for fname, _finfo in type(value).model_fields.items():
fval = getattr(value, fname, None)
formatted = _format_value(fval, rich=False, field_name=fname)
if formatted != "[not set]":
parts.append(f"{fname}={formatted}")
return ", ".join(parts) if parts else ("[dim]not set[/dim]" if rich else "[not set]")
if isinstance(value, list): if isinstance(value, list):
return ", ".join(str(v) for v in value) return ", ".join(str(v) for v in value)
if isinstance(value, dict): if isinstance(value, dict):
@@ -543,6 +551,7 @@ def _configure_pydantic_model(
return items + ["[Done]"] return items + ["[Done]"]
while True: while True:
console.clear()
_show_config_panel(display_name, working_model, fields) _show_config_panel(display_name, working_model, fields)
choices = get_choices() choices = get_choices()
answer = _select_with_back("Select field to configure:", choices) answer = _select_with_back("Select field to configure:", choices)
@@ -688,7 +697,6 @@ def _configure_provider(config: Config, provider_name: str) -> None:
def _configure_providers(config: Config) -> None: def _configure_providers(config: Config) -> None:
"""Configure LLM providers.""" """Configure LLM providers."""
_show_section_header("LLM Providers", "Select a provider to configure API key and endpoint")
def get_provider_choices() -> list[str]: def get_provider_choices() -> list[str]:
"""Build provider choices with config status indicators.""" """Build provider choices with config status indicators."""
@@ -703,6 +711,8 @@ def _configure_providers(config: Config) -> None:
while True: while True:
try: try:
console.clear()
_show_section_header("LLM Providers", "Select a provider to configure API key and endpoint")
choices = get_provider_choices() choices = get_provider_choices()
answer = _select_with_back("Select provider:", choices) answer = _select_with_back("Select provider:", choices)
@@ -738,18 +748,9 @@ def _get_channel_info() -> dict[str, tuple[str, type[BaseModel]]]:
for name, channel_cls in discover_all().items(): for name, channel_cls in discover_all().items():
try: try:
mod = importlib.import_module(f"nanobot.channels.{name}") mod = importlib.import_module(f"nanobot.channels.{name}")
config_cls = next( config_name = channel_cls.__name__.replace("Channel", "Config")
( config_cls = getattr(mod, config_name, None)
attr if config_cls and isinstance(config_cls, type) and issubclass(config_cls, BaseModel):
for attr in vars(mod).values()
if isinstance(attr, type)
and issubclass(attr, BaseModel)
and attr is not BaseModel
and attr.__name__.endswith("Config")
),
None,
)
if config_cls:
display_name = getattr(channel_cls, "display_name", name.capitalize()) display_name = getattr(channel_cls, "display_name", name.capitalize())
result[name] = (display_name, config_cls) result[name] = (display_name, config_cls)
except Exception: except Exception:
@@ -795,13 +796,13 @@ def _configure_channel(config: Config, channel_name: str) -> None:
def _configure_channels(config: Config) -> None: def _configure_channels(config: Config) -> None:
"""Configure chat channels.""" """Configure chat channels."""
_show_section_header("Chat Channels", "Select a channel to configure connection settings")
channel_names = list(_get_channel_names().keys()) channel_names = list(_get_channel_names().keys())
choices = channel_names + ["<- Back"] choices = channel_names + ["<- Back"]
while True: while True:
try: try:
console.clear()
_show_section_header("Chat Channels", "Select a channel to configure connection settings")
answer = _select_with_back("Select channel:", choices) answer = _select_with_back("Select channel:", choices)
if answer is _BACK_PRESSED or answer is None or answer == "<- Back": if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
@@ -842,8 +843,6 @@ def _configure_general_settings(config: Config, section: str) -> None:
if not meta: if not meta:
return return
display_name, subtitle, skip = meta display_name, subtitle, skip = meta
_show_section_header(section, subtitle)
model = _SETTINGS_GETTER[section](config) model = _SETTINGS_GETTER[section](config)
updated = _configure_pydantic_model(model, display_name, skip_fields=skip) updated = _configure_pydantic_model(model, display_name, skip_fields=skip)
if updated is not None: if updated is not None:
@@ -975,6 +974,7 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
config = base_config.model_copy(deep=True) config = base_config.model_copy(deep=True)
while True: while True:
console.clear()
_show_main_menu_header() _show_main_menu_header()
try: try:

View File

@@ -38,14 +38,7 @@ class AgentDefaults(Base):
context_window_tokens: int = 65_536 context_window_tokens: int = 65_536
temperature: float = 0.1 temperature: float = 0.1
max_tool_iterations: int = 40 max_tool_iterations: int = 40
# Deprecated compatibility field: accepted from old configs but ignored at runtime. reasoning_effort: str | None = None # low / medium / high - enables LLM thinking mode
memory_window: int | None = Field(default=None, exclude=True)
reasoning_effort: str | None = None # low / medium / high — enables LLM thinking mode
@property
def should_warn_deprecated_memory_window(self) -> bool:
"""Return True when old memoryWindow is present without contextWindowTokens."""
return self.memory_window is not None and "context_window_tokens" not in self.model_fields_set
class AgentsConfig(Base): class AgentsConfig(Base):

View File

@@ -452,14 +452,15 @@ def test_agent_workspace_override_wins_over_config_workspace(mock_agent_runtime,
assert mock_agent_runtime["agent_loop_cls"].call_args.kwargs["workspace"] == workspace_path assert mock_agent_runtime["agent_loop_cls"].call_args.kwargs["workspace"] == workspace_path
def test_agent_warns_about_deprecated_memory_window(mock_agent_runtime): def test_agent_hints_about_deprecated_memory_window(mock_agent_runtime, tmp_path):
mock_agent_runtime["config"].agents.defaults.memory_window = 100 config_file = tmp_path / "config.json"
config_file.write_text(json.dumps({"agents": {"defaults": {"memoryWindow": 42}}}))
result = runner.invoke(app, ["agent", "-m", "hello"]) result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)])
assert result.exit_code == 0 assert result.exit_code == 0
assert "memoryWindow" in result.stdout assert "memoryWindow" in result.stdout
assert "contextWindowTokens" in result.stdout assert "no longer used" in result.stdout
def test_gateway_uses_workspace_from_config_by_default(monkeypatch, tmp_path: Path) -> None: def test_gateway_uses_workspace_from_config_by_default(monkeypatch, tmp_path: Path) -> None:
@@ -523,28 +524,6 @@ def test_gateway_workspace_option_overrides_config(monkeypatch, tmp_path: Path)
assert config.workspace_path == override assert config.workspace_path == override
def test_gateway_warns_about_deprecated_memory_window(monkeypatch, tmp_path: Path) -> None:
config_file = tmp_path / "instance" / "config.json"
config_file.parent.mkdir(parents=True)
config_file.write_text("{}")
config = Config()
config.agents.defaults.memory_window = 100
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
monkeypatch.setattr(
"nanobot.cli.commands._make_provider",
lambda _config: (_ for _ in ()).throw(_StopGatewayError("stop")),
)
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
assert isinstance(result.exception, _StopGatewayError)
assert "memoryWindow" in result.stdout
assert "contextWindowTokens" in result.stdout
def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Path) -> None: def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Path) -> None:
config_file = tmp_path / "instance" / "config.json" config_file = tmp_path / "instance" / "config.json"
config_file.parent.mkdir(parents=True) config_file.parent.mkdir(parents=True)

View File

@@ -3,7 +3,7 @@ import json
from nanobot.config.loader import load_config, save_config from nanobot.config.loader import load_config, save_config
def test_load_config_keeps_max_tokens_and_warns_on_legacy_memory_window(tmp_path) -> None: def test_load_config_keeps_max_tokens_and_ignores_legacy_memory_window(tmp_path) -> None:
config_path = tmp_path / "config.json" config_path = tmp_path / "config.json"
config_path.write_text( config_path.write_text(
json.dumps( json.dumps(
@@ -23,7 +23,7 @@ def test_load_config_keeps_max_tokens_and_warns_on_legacy_memory_window(tmp_path
assert config.agents.defaults.max_tokens == 1234 assert config.agents.defaults.max_tokens == 1234
assert config.agents.defaults.context_window_tokens == 65_536 assert config.agents.defaults.context_window_tokens == 65_536
assert config.agents.defaults.should_warn_deprecated_memory_window is True assert not hasattr(config.agents.defaults, "memory_window")
def test_save_config_writes_context_window_tokens_but_not_memory_window(tmp_path) -> None: def test_save_config_writes_context_window_tokens_but_not_memory_window(tmp_path) -> None:
@@ -52,7 +52,7 @@ def test_save_config_writes_context_window_tokens_but_not_memory_window(tmp_path
assert "memoryWindow" not in defaults assert "memoryWindow" not in defaults
def test_onboard_refresh_rewrites_legacy_config_template(tmp_path, monkeypatch) -> None: def test_onboard_does_not_crash_with_legacy_memory_window(tmp_path, monkeypatch) -> None:
config_path = tmp_path / "config.json" config_path = tmp_path / "config.json"
workspace = tmp_path / "workspace" workspace = tmp_path / "workspace"
config_path.write_text( config_path.write_text(
@@ -78,12 +78,6 @@ def test_onboard_refresh_rewrites_legacy_config_template(tmp_path, monkeypatch)
result = runner.invoke(app, ["onboard"], input="n\n") result = runner.invoke(app, ["onboard"], input="n\n")
assert result.exit_code == 0 assert result.exit_code == 0
assert "contextWindowTokens" in result.stdout
saved = json.loads(config_path.read_text(encoding="utf-8"))
defaults = saved["agents"]["defaults"]
assert defaults["maxTokens"] == 3333
assert defaults["contextWindowTokens"] == 65_536
assert "memoryWindow" not in defaults
def test_onboard_refresh_backfills_missing_channel_fields(tmp_path, monkeypatch) -> None: def test_onboard_refresh_backfills_missing_channel_fields(tmp_path, monkeypatch) -> None:

View File

@@ -182,7 +182,7 @@ class TestConsolidationTriggerConditions:
"""Test consolidation trigger conditions and logic.""" """Test consolidation trigger conditions and logic."""
def test_consolidation_needed_when_messages_exceed_window(self): def test_consolidation_needed_when_messages_exceed_window(self):
"""Test consolidation logic: should trigger when messages > memory_window.""" """Test consolidation logic: should trigger when messages exceed the window."""
session = create_session_with_messages("test:trigger", 60) session = create_session_with_messages("test:trigger", 60)
total_messages = len(session.messages) total_messages = len(session.messages)