Merge branch 'main' into fix/workspace-scoped-cron-store

Keep cron state workspace-scoped while only migrating legacy jobs into the default workspace. This preserves seamless upgrades for existing installs without polluting intentionally new workspaces.
This commit is contained in:
Xubin Ren
2026-03-24 02:41:58 +00:00
70 changed files with 6550 additions and 606 deletions
+2
View File
@@ -7,6 +7,7 @@ from nanobot.config.paths import (
get_cron_dir,
get_data_dir,
get_legacy_sessions_dir,
is_default_workspace,
get_logs_dir,
get_media_dir,
get_runtime_subdir,
@@ -24,6 +25,7 @@ __all__ = [
"get_cron_dir",
"get_logs_dir",
"get_workspace_path",
"is_default_workspace",
"get_cli_history_path",
"get_bridge_install_dir",
"get_legacy_sessions_dir",
+7 -5
View File
@@ -3,8 +3,10 @@
import json
from pathlib import Path
from nanobot.config.schema import Config
import pydantic
from loguru import logger
from nanobot.config.schema import Config
# Global variable to store current config path (for multi-instance support)
_current_config_path: Path | None = None
@@ -41,9 +43,9 @@ def load_config(config_path: Path | None = None) -> Config:
data = json.load(f)
data = _migrate_config(data)
return Config.model_validate(data)
except (json.JSONDecodeError, ValueError) as e:
print(f"Warning: Failed to load config from {path}: {e}")
print("Using default configuration.")
except (json.JSONDecodeError, ValueError, pydantic.ValidationError) as e:
logger.warning(f"Failed to load config from {path}: {e}")
logger.warning("Using default configuration.")
return Config()
@@ -59,7 +61,7 @@ def save_config(config: Config, config_path: Path | None = None) -> None:
path = config_path or get_config_path()
path.parent.mkdir(parents=True, exist_ok=True)
data = config.model_dump(by_alias=True)
data = config.model_dump(mode="json", by_alias=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
+7
View File
@@ -40,6 +40,13 @@ def get_workspace_path(workspace: str | None = None) -> Path:
return ensure_dir(path)
def is_default_workspace(workspace: str | Path | None) -> bool:
"""Return whether a workspace resolves to nanobot's default workspace path."""
current = Path(workspace).expanduser() if workspace is not None else Path.home() / ".nanobot" / "workspace"
default = Path.home() / ".nanobot" / "workspace"
return current.resolve(strict=False) == default.resolve(strict=False)
def get_cli_history_path() -> Path:
"""Return the shared CLI history file path."""
return Path.home() / ".nanobot" / "history" / "cli_history"
+8 -11
View File
@@ -18,6 +18,7 @@ class ChannelsConfig(Base):
Built-in and plugin channel configs are stored as extra fields (dicts).
Each channel parses its own config in __init__.
Per-channel "streaming": true enables streaming output (requires send_delta impl).
"""
model_config = ConfigDict(extra="allow")
@@ -38,14 +39,7 @@ class AgentDefaults(Base):
context_window_tokens: int = 65_536
temperature: float = 0.1
max_tool_iterations: int = 40
# Deprecated compatibility field: accepted from old configs but ignored at runtime.
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
reasoning_effort: str | None = None # low / medium / high - enables LLM thinking mode
class AgentsConfig(Base):
@@ -76,17 +70,19 @@ class ProvidersConfig(Base):
dashscope: ProviderConfig = Field(default_factory=ProviderConfig)
vllm: ProviderConfig = Field(default_factory=ProviderConfig)
ollama: ProviderConfig = Field(default_factory=ProviderConfig) # Ollama local models
ovms: ProviderConfig = Field(default_factory=ProviderConfig) # OpenVINO Model Server (OVMS)
gemini: ProviderConfig = Field(default_factory=ProviderConfig)
moonshot: ProviderConfig = Field(default_factory=ProviderConfig)
minimax: ProviderConfig = Field(default_factory=ProviderConfig)
mistral: ProviderConfig = Field(default_factory=ProviderConfig)
aihubmix: ProviderConfig = Field(default_factory=ProviderConfig) # AiHubMix API gateway
siliconflow: ProviderConfig = Field(default_factory=ProviderConfig) # SiliconFlow (硅基流动)
volcengine: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine (火山引擎)
volcengine_coding_plan: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine Coding Plan
byteplus: ProviderConfig = Field(default_factory=ProviderConfig) # BytePlus (VolcEngine international)
byteplus_coding_plan: ProviderConfig = Field(default_factory=ProviderConfig) # BytePlus Coding Plan
openai_codex: ProviderConfig = Field(default_factory=ProviderConfig) # OpenAI Codex (OAuth)
github_copilot: ProviderConfig = Field(default_factory=ProviderConfig) # Github Copilot (OAuth)
openai_codex: ProviderConfig = Field(default_factory=ProviderConfig, exclude=True) # OpenAI Codex (OAuth)
github_copilot: ProviderConfig = Field(default_factory=ProviderConfig, exclude=True) # Github Copilot (OAuth)
class HeartbeatConfig(Base):
@@ -94,6 +90,7 @@ class HeartbeatConfig(Base):
enabled: bool = True
interval_s: int = 30 * 60 # 30 minutes
keep_recent_messages: int = 8
class GatewayConfig(Base):
@@ -125,10 +122,10 @@ class WebToolsConfig(Base):
class ExecToolConfig(Base):
"""Shell exec tool configuration."""
enable: bool = True
timeout: int = 60
path_append: str = ""
class MCPServerConfig(Base):
"""MCP server connection configuration (stdio or HTTP)."""