From ec00482157cb5dec2b3fbdfebbce3f85e176b9fd Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Thu, 16 Apr 2026 19:16:25 -0600 Subject: [PATCH] feat(supervisor): run pending maint tasks before spawning children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the journal-writer/migration race by running maintenance tasks in the supervisor's main process before any writer child (convey, sense, cortex) is spawned. Migration code moves from convey/maint.py to think/maint.py (clean break, no shim) — convey's startup block and import are removed; sol maint CLI and the --skip-maint flag are unchanged (flag is now a no-op). Phase 2 will add standalone-writer guards so CLIs like sol dream refuse to start without a running supervisor. Co-Authored-By: Claude Opus 4.7 (1M context) --- convey/cli.py | 9 --------- convey/maint_cli.py | 2 +- docs/APPS.md | 4 ++-- tests/test_maint.py | 8 ++++---- {convey => think}/maint.py | 0 think/supervisor.py | 17 +++++++++++++++++ 6 files changed, 24 insertions(+), 16 deletions(-) rename {convey => think}/maint.py (100%) diff --git a/convey/cli.py b/convey/cli.py index 56d4c9009..ed696db07 100644 --- a/convey/cli.py +++ b/convey/cli.py @@ -63,8 +63,6 @@ def run_service( def main() -> None: """Main CLI entry point for convey command.""" - from pathlib import Path - from think.utils import ( get_journal, setup_cli, @@ -72,7 +70,6 @@ def main() -> None: ) from . import create_app - from .maint import run_pending_tasks parser = argparse.ArgumentParser(description="Convey web interface") parser.add_argument( @@ -89,12 +86,6 @@ def main() -> None: args = setup_cli(parser) journal = get_journal() - # Run pending maintenance tasks before starting - if not args.skip_maint: - ran, succeeded = run_pending_tasks(Path(journal)) - if ran > 0: - logger.info(f"Completed {succeeded}/{ran} maintenance task(s)") - app = create_app(journal) password = _resolve_config_password_hash() if password: diff --git a/convey/maint_cli.py b/convey/maint_cli.py index e79ace984..3becf89a7 100644 --- a/convey/maint_cli.py +++ b/convey/maint_cli.py @@ -20,7 +20,7 @@ from pathlib import Path from think.utils import get_journal, setup_cli -from .maint import ( +from think.maint import ( get_state_file, get_task_by_name, get_task_status, diff --git a/docs/APPS.md b/docs/APPS.md index b569bfc37..0ee09a9aa 100644 --- a/docs/APPS.md +++ b/docs/APPS.md @@ -455,7 +455,7 @@ apps/my_app/talent/my-skill/ ### 11. `maint/` - Maintenance Tasks -Define one-time maintenance scripts that run automatically on Convey startup. +Define one-time maintenance scripts that run automatically when supervisor starts. **Key Points:** - Create `maint/` directory with standalone Python scripts (each with a `main()` function) @@ -468,7 +468,7 @@ Define one-time maintenance scripts that run automatically on Convey startup. **Reference implementations:** - Example task: `apps/entities/maint/001_migrate_to_journal_entities.py` - real migration task demonstrating maint patterns -- Discovery logic: `convey/maint.py` - `discover_tasks()`, `run_task()` +- Discovery logic: `think/maint.py` - `discover_tasks()`, `run_task()` --- diff --git a/tests/test_maint.py b/tests/test_maint.py index 94996e3dc..dc9e4fe1c 100644 --- a/tests/test_maint.py +++ b/tests/test_maint.py @@ -8,7 +8,7 @@ from pathlib import Path import pytest -from convey.maint import ( +from think.maint import ( MaintTask, get_state_file, get_task_status, @@ -115,7 +115,7 @@ class TestListTasks: def mock_discover_tasks(): return tasks - monkeypatch.setattr("convey.maint.discover_tasks", mock_discover_tasks) + monkeypatch.setattr("think.maint.discover_tasks", mock_discover_tasks) # Success task with timestamp success_file = get_state_file(temp_journal, "chat", "done") @@ -164,7 +164,7 @@ class TestListTasks: def mock_discover_tasks(): return tasks - monkeypatch.setattr("convey.maint.discover_tasks", mock_discover_tasks) + monkeypatch.setattr("think.maint.discover_tasks", mock_discover_tasks) # Success task with line events and duration success_file = get_state_file(temp_journal, "chat", "done") @@ -199,7 +199,7 @@ class TestListTasks: def mock_discover_tasks(): return tasks - monkeypatch.setattr("convey.maint.discover_tasks", mock_discover_tasks) + monkeypatch.setattr("think.maint.discover_tasks", mock_discover_tasks) # In-progress task: exec event but no exit event state_file = get_state_file(temp_journal, "chat", "running") diff --git a/convey/maint.py b/think/maint.py similarity index 100% rename from convey/maint.py rename to think/maint.py diff --git a/think/supervisor.py b/think/supervisor.py index 1624b2bed..affa9ab75 100644 --- a/think/supervisor.py +++ b/think/supervisor.py @@ -21,6 +21,7 @@ from desktop_notifier import DesktopNotifier, Urgency from think import routines, scheduler from think.callosum import CallosumConnection, CallosumServer +from think.maint import run_pending_tasks from think.runner import DailyLogWriter from think.runner import ManagedProcess as RunnerManagedProcess from think.utils import ( @@ -1435,6 +1436,22 @@ def main() -> None: # Remote mode: run sync instead of local processing _is_remote_mode = bool(args.remote) + # Run pending journal-maintenance tasks before spawning any writer children. + # Callosum isn't up yet (emit_fn=None); migrations log through supervisor's logger only. + try: + ran, succeeded = run_pending_tasks(journal_path, emit_fn=None) + if ran > 0: + if ran == succeeded: + logging.info("Completed %d/%d maintenance task(s)", succeeded, ran) + else: + logging.error( + "Maintenance tasks completed with failures: %d/%d succeeded", + succeeded, + ran, + ) + except Exception: + logging.exception("Maintenance runner raised; continuing startup") + # Start Callosum in-process first - it's the message bus that other services depend on try: start_callosum_in_process() -- 2.51.2