Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ curl http://localhost:3000/api/v1/health # backend (via web proxy)

```text
src/synthorg/
api/ # Litestar REST + WebSocket API (controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation)), AppState hot-reload slots (provider_registry, model_router with swap methods, provider_management), settings dispatcher lifecycle
api/ # Litestar REST + WebSocket API (controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation)), AppState hot-reload slots (provider_registry, model_router with swap methods, provider_management), settings dispatcher lifecycle, service auto-wiring (auto_wire.py: Phase 1 at construction -- message bus/cost tracker/provider registry/task engine; Phase 2 in on_startup after persistence connects -- settings service + config resolver + provider management)
auth/ # Authentication subpackage (controller, service, middleware, JWT + API key + WS ticket store, models, config)
backup/ # Backup and restore -- scheduled/manual/lifecycle backups of persistence DB, agent memory, and company config. BackupService orchestrator, BackupScheduler (periodic asyncio task), RetentionManager (count + age pruning), tar.gz compression, SHA-256 checksums, manifest tracking, validated restore with atomic rollback and safety backup
handlers/ # ComponentHandler protocol + concrete handlers: PersistenceComponentHandler (SQLite VACUUM INTO), MemoryComponentHandler (copytree), ConfigComponentHandler (copy2)
Expand Down
104 changes: 80 additions & 24 deletions src/synthorg/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from synthorg.api.auth.middleware import create_auth_middleware_class
from synthorg.api.auth.secret import resolve_jwt_secret
from synthorg.api.auth.service import AuthService
from synthorg.api.auto_wire import auto_wire_phase1, auto_wire_settings
from synthorg.api.bus_bridge import MessageBusBridge
from synthorg.api.channels import (
CHANNEL_APPROVALS,
Expand Down Expand Up @@ -64,6 +65,7 @@
from synthorg.persistence.config import PersistenceConfig, SQLiteConfig
from synthorg.persistence.factory import create_backend
from synthorg.persistence.protocol import PersistenceBackend # noqa: TC001
from synthorg.providers.registry import ProviderRegistry # noqa: TC001
from synthorg.settings.dispatcher import SettingsChangeDispatcher
from synthorg.settings.subscribers import (
BackupSettingsSubscriber,
Expand Down Expand Up @@ -176,7 +178,7 @@ async def _ticket_cleanup_loop(app_state: AppState) -> None:
)


def _build_lifecycle( # noqa: PLR0913
def _build_lifecycle( # noqa: PLR0913, C901, D417

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The D417 noqa indicates that the docstring for _build_lifecycle is missing descriptions for some arguments. It's important to keep docstrings up-to-date for maintainability and clarity, especially for complex functions. Please add descriptions for should_auto_wire_settings and effective_config to the docstring.

persistence: PersistenceBackend | None,
message_bus: MessageBus | None,
bridge: MessageBusBridge | None,
Expand All @@ -185,16 +187,26 @@ def _build_lifecycle( # noqa: PLR0913
meeting_scheduler: MeetingScheduler | None,
backup_service: BackupService | None,
app_state: AppState,
*,
should_auto_wire_settings: bool = False,
effective_config: RootConfig | None = None,
) -> tuple[
Sequence[Callable[[], Awaitable[None]]],
Sequence[Callable[[], Awaitable[None]]],
]:
"""Build startup and shutdown hooks.

Args:
should_auto_wire_settings: When ``True``, Phase 2 auto-wiring
creates ``SettingsService`` + dispatcher after persistence
connects.
effective_config: Root config needed for Phase 2 auto-wiring.

Returns:
A tuple of (on_startup, on_shutdown) callback lists.
"""
_ticket_cleanup_task: asyncio.Task[None] | None = None
_auto_wired_dispatcher: SettingsChangeDispatcher | None = None

def _on_cleanup_task_done(task: asyncio.Task[None]) -> None:
"""Log unexpected cleanup-task death."""
Expand All @@ -209,7 +221,7 @@ def _on_cleanup_task_done(task: asyncio.Task[None]) -> None:
)

async def on_startup() -> None:
nonlocal _ticket_cleanup_task
nonlocal _ticket_cleanup_task, _auto_wired_dispatcher
logger.info(API_APP_STARTUP, version=__version__)
await _safe_startup(
persistence,
Expand All @@ -221,20 +233,60 @@ async def on_startup() -> None:
backup_service,
app_state,
)
# Phase 2 auto-wire: SettingsService (needs connected persistence)
if (
should_auto_wire_settings
and persistence is not None
and effective_config is not None
and not app_state.has_settings_service
):
try:
_auto_wired_dispatcher = await auto_wire_settings(
persistence,
message_bus,
effective_config,
app_state,
backup_service,
_build_settings_dispatcher,
)
except MemoryError, RecursionError:
raise
Comment on lines +259 to +260

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The except MemoryError, RecursionError: syntax is deprecated in Python 3.10 and removed in Python 3.12. It should be except (MemoryError, RecursionError):.

            except (MemoryError, RecursionError):

except Exception:
logger.exception(
API_APP_STARTUP,
error="Phase 2 auto-wire failed",
)
await _safe_shutdown(
task_engine,
meeting_scheduler,
backup_service,
settings_dispatcher,
bridge,
message_bus,
persistence,
)
raise
_ticket_cleanup_task = asyncio.create_task(
_ticket_cleanup_loop(app_state),
name="ws-ticket-cleanup",
)
_ticket_cleanup_task.add_done_callback(_on_cleanup_task_done)

async def on_shutdown() -> None:
nonlocal _ticket_cleanup_task
nonlocal _ticket_cleanup_task, _auto_wired_dispatcher
if _ticket_cleanup_task is not None:
_ticket_cleanup_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await _ticket_cleanup_task
_ticket_cleanup_task = None
logger.info(API_APP_SHUTDOWN, version=__version__)
if _auto_wired_dispatcher is not None:
await _try_stop(
_auto_wired_dispatcher.stop(),
API_APP_SHUTDOWN,
"Failed to stop auto-wired settings dispatcher",
)
_auto_wired_dispatcher = None
await _safe_shutdown(
task_engine,
meeting_scheduler,
Expand Down Expand Up @@ -614,16 +666,13 @@ def create_app( # noqa: PLR0913
meeting_scheduler: MeetingScheduler | None = None,
performance_tracker: PerformanceTracker | None = None,
settings_service: SettingsService | None = None,
provider_registry: ProviderRegistry | None = None,
) -> Litestar:
"""Create and configure the Litestar application.

All parameters are optional for testing — provide fakes via
keyword arguments.

When ``persistence`` is not provided, the factory checks
``SYNTHORG_DB_PATH`` in the environment and auto-creates a
SQLite backend if set (used by the Docker compose template).
Explicit ``persistence`` always takes precedence.
All parameters are optional for testing -- provide fakes via
keyword arguments. Services not explicitly provided are
auto-wired from config and environment variables.

Args:
config: Root company configuration.
Expand All @@ -639,6 +688,7 @@ def create_app( # noqa: PLR0913
meeting_scheduler: Meeting scheduler.
performance_tracker: Performance tracking service.
settings_service: Settings service for runtime config.
provider_registry: Provider registry.

Returns:
Configured Litestar application.
Expand Down Expand Up @@ -677,20 +727,19 @@ def create_app( # noqa: PLR0913
db_path=db_path,
)

if (
persistence is None
or message_bus is None
or cost_tracker is None
or task_engine is None
or settings_service is None
):
msg = (
"create_app called without persistence, message_bus, "
"cost_tracker, task_engine, and/or settings_service — "
"controllers accessing missing services will return 503. "
"Use test fakes for testing."
)
logger.warning(API_APP_STARTUP, note=msg)
# ── Phase 1 auto-wire: services that don't need connected persistence ──
phase1 = auto_wire_phase1(
effective_config=effective_config,
persistence=persistence,
message_bus=message_bus,
cost_tracker=cost_tracker,
task_engine=task_engine,
provider_registry=provider_registry,
)
message_bus = phase1.message_bus
cost_tracker = phase1.cost_tracker
task_engine = phase1.task_engine
provider_registry = phase1.provider_registry

channels_plugin = create_channels_plugin()
expire_callback = _make_expire_callback(channels_plugin)
Expand Down Expand Up @@ -718,6 +767,7 @@ def create_app( # noqa: PLR0913
meeting_scheduler=meeting_scheduler,
performance_tracker=performance_tracker,
settings_service=settings_service,
provider_registry=provider_registry,
startup_time=time.monotonic(),
)

Expand All @@ -743,6 +793,10 @@ def create_app( # noqa: PLR0913
guards=[require_password_changed],
)

# Phase 2 auto-wiring flag: SettingsService needs connected persistence
# and is created in on_startup after _init_persistence().
_should_auto_wire = settings_service is None and persistence is not None

startup, shutdown = _build_lifecycle(
persistence,
message_bus,
Expand All @@ -752,6 +806,8 @@ def create_app( # noqa: PLR0913
meeting_scheduler,
backup_service,
app_state,
should_auto_wire_settings=_should_auto_wire,
effective_config=effective_config,
)

return Litestar(
Expand Down
Loading
Loading