feat(jobs): allow disabling cron schedules independently

This commit is contained in:
jinli.yl 2026-09-30 17:40:55 +08:00
parent 67135cfc57
commit 65546cb357
12 changed files with 94 additions and 5 deletions

View file

@ -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.<name>.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:

View file

@ -79,6 +79,17 @@ base_url: ${LLM_BASE_URL:-https://example.com/v1}
`session_dir` 必须保持 workspace-relative。其他 workspace 子目录也应使用清晰、稳定的相对名称。
## Cron 调度
`jobs.<name>.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 接口:

View file

@ -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

View file

@ -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 安装插件

View file

@ -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

View file

@ -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

View file

@ -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}")

View file

@ -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 {}

View file

@ -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

View file

@ -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):

View file

@ -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():

View file

@ -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):