diff --git a/docs/en/configuration.md b/docs/en/configuration.md index 88e6f737..29163b02 100644 --- a/docs/en/configuration.md +++ b/docs/en/configuration.md @@ -74,6 +74,19 @@ Keep secrets in `.env` or the process environment, never in committed configurat `session_dir` must remain workspace-relative. +## Cron scheduling + +`jobs..enable_schedule` defaults to `true` and controls automatic startup of cron jobs only. +Set it to `false` when a host plugin owns the schedule: + +```bash +reme start jobs.dream_cron.enable_schedule=false +``` + +Restart the service to apply the override. The `auto_dream` API remains available, and other jobs continue running. +`enable_serve` separately controls service exposure; it does not disable scheduling. Disabled cron jobs retain their +configuration but do not start their steps or background tasks. The switch has no effect on other job types. + ## LLM The default LLM uses an OpenAI-compatible interface: diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 58b7920e..04548e00 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -79,6 +79,17 @@ base_url: ${LLM_BASE_URL:-https://example.com/v1} `session_dir` 必须保持 workspace-relative。其他 workspace 子目录也应使用清晰、稳定的相对名称。 +## Cron 调度 + +`jobs..enable_schedule` 默认为 `true`,仅控制 cron Job 是否自动启动。由宿主插件负责调度时,可以关闭服务端对应任务: + +```bash +reme start jobs.dream_cron.enable_schedule=false +``` + +重启服务后生效。`auto_dream` 接口仍可调用,其他 Job 继续运行。`enable_serve` 独立控制接口暴露,不控制调度。 +关闭的 cron 保留配置,但不启动 Steps 或后台任务;此开关对其他 Job 类型没有影响。 + ## LLM 配置 默认 LLM 使用 OpenAI-compatible 接口: diff --git a/integrations/dsh/README.md b/integrations/dsh/README.md index b78a1e6e..a0a57080 100644 --- a/integrations/dsh/README.md +++ b/integrations/dsh/README.md @@ -41,9 +41,14 @@ host-specific plugin. ```bash reme start workspace_dir=/absolute/path/to/your/reme-workspace \ - service.host=127.0.0.1 service.port=3457 + service.host=127.0.0.1 service.port=3457 \ + jobs.dream_cron.enable_schedule=false ``` +The plugin owns the daily Dream schedule by default, so this disables ReMe's `dream_cron` while keeping the +`auto_dream` API available. To use ReMe scheduling instead, omit this override and set the plugin's +`autoDreamEnabled` to `false`. Shared services should have only one scheduler. + For development and screenshots, use an isolated directory outside the repository, such as `/tmp/reme-dsh-demo`. Do not write runtime memory into the repository's `.reme/` directory. ### 3.2 Install the DSH bundle diff --git a/integrations/dsh/README_ZH.md b/integrations/dsh/README_ZH.md index 6f4954ec..8231e2d7 100644 --- a/integrations/dsh/README_ZH.md +++ b/integrations/dsh/README_ZH.md @@ -40,9 +40,13 @@ ReMe HTTP 服务默认监听 `http://127.0.0.1:2333`,不使用 API Key 认证 ```bash reme start workspace_dir=/absolute/path/to/your/reme-workspace \ - service.host=127.0.0.1 service.port=3457 + service.host=127.0.0.1 service.port=3457 \ + jobs.dream_cron.enable_schedule=false ``` +插件默认负责每日 Dream 调度,因此这里关闭 ReMe 的 `dream_cron`,保留 `auto_dream` 接口。 +若改由 ReMe 调度,省略该覆盖并将插件的 `autoDreamEnabled` 设为 `false`。多个宿主共享服务时,只启用一个调度方。 + 开发或截图测试时建议使用仓库外的独立目录,例如 `/tmp/reme-dsh-demo`,不要把运行时记忆写入 ReMe 仓库自身的 `.reme/`。 ### 3.2 安装插件 diff --git a/integrations/openclaw/README.md b/integrations/openclaw/README.md index 6615a168..f1afaae0 100644 --- a/integrations/openclaw/README.md +++ b/integrations/openclaw/README.md @@ -40,9 +40,14 @@ pip install "reme-ai[core]" reme start \ workspace_dir=/absolute/path/to/reme-workspace \ service.host=127.0.0.1 \ - service.port=3458 + service.port=3458 \ + jobs.dream_cron.enable_schedule=false ``` +The plugin owns the daily Dream schedule by default, so this disables ReMe's `dream_cron` while keeping the +`auto_dream` API available. To use ReMe scheduling instead, omit this override and set the plugin's +`autoDreamEnabled` to `false`. Shared services should have only one scheduler. + Verify the service without exposing configuration or credentials: ```bash diff --git a/integrations/openclaw/README_ZH.md b/integrations/openclaw/README_ZH.md index 79d681da..f27fbc3b 100644 --- a/integrations/openclaw/README_ZH.md +++ b/integrations/openclaw/README_ZH.md @@ -37,9 +37,13 @@ pip install "reme-ai[core]" reme start \ workspace_dir=/absolute/path/to/reme-workspace \ service.host=127.0.0.1 \ - service.port=3458 + service.port=3458 \ + jobs.dream_cron.enable_schedule=false ``` +插件默认负责每日 Dream 调度,因此这里关闭 ReMe 的 `dream_cron`,保留 `auto_dream` 接口。 +若改由 ReMe 调度,省略该覆盖并将插件的 `autoDreamEnabled` 设为 `false`。多个宿主共享服务时,只启用一个调度方。 + 不读取或输出模型配置即可验证服务: ```bash diff --git a/reme/application.py b/reme/application.py index b4b0cb69..3ef457f0 100644 --- a/reme/application.py +++ b/reme/application.py @@ -206,6 +206,9 @@ class Application(BaseComponent): async def _start_one(self, c: BaseComponent) -> None: """Start one component and record it for ordered shutdown.""" + if isinstance(c, CronJob) and not c.enable_schedule: + self.logger.info(f"Skipping disabled cron job: {c.name}") + return try: if isinstance(c, BackgroundJob): self.logger.info(f"Starting background job: {c.name}") diff --git a/reme/components/job/base_job.py b/reme/components/job/base_job.py index 32475ddd..7c5e5138 100644 --- a/reme/components/job/base_job.py +++ b/reme/components/job/base_job.py @@ -46,6 +46,7 @@ class BaseJob(BaseComponent): enable_serve: bool = True, **kwargs, ): + kwargs.pop("enable_schedule", None) super().__init__(**kwargs) self.description = description self.parameters = parameters or {} diff --git a/reme/components/job/cron_job.py b/reme/components/job/cron_job.py index a72a7878..1caf7194 100644 --- a/reme/components/job/cron_job.py +++ b/reme/components/job/cron_job.py @@ -13,9 +13,10 @@ from ...schema import Response class CronJob(BackgroundJob): """Run this job's own steps on a cron expression.""" - def __init__(self, cron: str, **kwargs): + def __init__(self, cron: str, enable_schedule: bool = True, **kwargs): super().__init__(**kwargs) self.cron_expr = cron + self.enable_schedule = enable_schedule async def _start(self) -> None: from croniter import croniter diff --git a/reme/schema/application_config.py b/reme/schema/application_config.py index 0800ef30..f5ceef55 100644 --- a/reme/schema/application_config.py +++ b/reme/schema/application_config.py @@ -24,6 +24,7 @@ class JobConfig(ComponentConfig): parameters: dict = Field(default_factory=dict, description="Job-level parameters") steps: list[ComponentConfig] = Field(default_factory=list, description="Ordered step configs") enable_serve: bool = Field(default=True, description="Whether to expose this job through the service layer") + enable_schedule: bool = Field(default=True, description="Whether to start automatic scheduling for a cron job") class ApplicationConfig(BaseModel): diff --git a/tests/unit/test_config_parser.py b/tests/unit/test_config_parser.py index 726df2e7..4d3eb594 100644 --- a/tests/unit/test_config_parser.py +++ b/tests/unit/test_config_parser.py @@ -10,8 +10,23 @@ from reme.config.config_parser import ( _read_config_file, parse_args, parse_dot_notation, + parse_kwargs, resolve_app_config, ) +from reme.schema import ApplicationConfig + + +def test_cli_disables_dream_schedule_without_disabling_manual_job(): + """A CLI schedule override preserves callable jobs and other default schedules.""" + cfg = ApplicationConfig( + **resolve_app_config(log_config=False, **parse_kwargs("jobs.dream_cron.enable_schedule=false")), + ) + + assert cfg.jobs["dream_cron"].enable_schedule is False + assert cfg.jobs["dream_cron"].backend == "cron" + assert cfg.jobs["auto_dream"].enable_serve is True + assert cfg.jobs["auto_dream"].steps + assert cfg.jobs["optimize_index_cron"].enable_schedule is True def test_load_builtin_config_by_filename_with_suffix(): diff --git a/tests/unit/test_job.py b/tests/unit/test_job.py index 7a0b7a2c..aaa7d8cc 100644 --- a/tests/unit/test_job.py +++ b/tests/unit/test_job.py @@ -384,6 +384,32 @@ def test_application_starts_jobs_base_stream_background_cron(): asyncio.run(run()) +@pytest.mark.parametrize("enabled", [True, False]) +def test_application_respects_cron_schedule_switch(enabled): + async def run(): + app = object.__new__(Application) + app._started_components = [] + app.logger = MagicMock() + job = CronJob(cron="0 23 * * *", enable_schedule=enabled, enable_serve=True, name="dream_cron") + job.start = AsyncMock() + + await app._start_one(job) + + assert job.start.await_count == int(enabled) + assert app._started_components == ([job] if enabled else []) + assert job.enable_serve is False + assert job._task is None + + for cls in (BaseJob, StreamJob, BackgroundJob): + other = cls(enable_schedule=False) + other.start = AsyncMock() + await app._start_one(other) + other.start.assert_awaited_once() + assert "enable_schedule" not in other.kwargs + + asyncio.run(run()) + + def test_application_start_failure_propagates_and_closes_started_components(): async def run(): class GoodComponent(BaseComponent):