API Startup Lifecycle¶
The API boots in two phases. Construction (the create_app body) wires synchronous services. On-startup (_build_lifecycle.on_startup) wires services that need a connected persistence backend. The ordering invariants below are load-bearing: getting them wrong races a dependency and 503s a controller forever, or poisons startup on a transient boot.
Binary preflight¶
run_binary_preflight (api/lifecycle_helpers/binary_preflight.py) runs first
in the construction phase, before anything is wired. The product shells out to a
handful of binaries, and an image missing one boots cleanly and fails at the
moment the feature is used, which for git is every single dispatch: workspace
provisioning is on the critical path of every task and
coordination.enable_workspace_isolation defaults on. The whole test suite is
blind to it, because tests run where git is on PATH.
The manifest is declarative, one record per binary, and holds exactly the
programs the backend needs and cannot obtain for itself. A missing one
raises RequiredBinaryMissingError and aborts the boot, naming the binary, the
apko package that provides it, and the subsystems it breaks. Refusing to start
is the honest outcome: the alternative is a backend that accepts work it can
never dispatch.
A record may also declare a min_version, because being on PATH is not the
same as being able to do the job. git declares 2.48, the release that added
worktree.useRelativePaths: git IGNORES a config key it does not know rather
than refusing it, so an older binary accepts the option, reports nothing, and
returns a worktree recording the backend's absolute path. The agent opens that
worktree through a different mount, and every git command it runs fails with a
"not a git repository" naming a path that exists on one side only. The version
is read by asking the binary; a version that cannot be read is logged and
allowed through, because not knowing is not evidence of an old one and
refusing on a line it could not read would take a working deployment down over
output the parser did not anticipate.
The probe is bounded at five seconds, so a binary that will not answer cannot hang the boot; it is treated exactly as one whose answer could not be read. It runs the path the presence check already resolved rather than the bare name, so the binary that answers is the one that was found: Windows searches the working directory ahead of PATH for a bare name, which would otherwise let the two halves disagree. The comparison runs over the shared prefix, so a longer version string still satisfies a shorter floor.
"Cannot be read" covers four cases, and the warning names which one, because
they need different things done about them: the probe timed out (a wedged
binary), it could not be spawned (one that vanished between the PATH lookup
and the probe), its output carried no version (a gap in the parser), or it
reported fewer components than the floor declares. The last is the one worth
stating explicitly, since it arrives wearing a number: tuple ordering would
call 2 lower than 2.48 and refuse the boot over a minor version nothing
ever reported, so a version too short to compare is treated as unread rather
than as old. Decoding is lenient for the same reason, as a strict decode
raises a ValueError that no handler here catches and would crash the boot on
the one path whose contract is that an unreadable version is survivable.
git is required always; pg_dump / pg_restore only when the
configured backend is Postgres, which is why the preflight is handed the
resolved backend name (empty when persistence has not resolved one, so only the
backend-independent binaries are demanded).
A binary the backend provisions at runtime is deliberately absent. Both tunnel
adapters download their vendor CLI on first start
(integrations.tunnel.cloudflared_download_enabled /
devtunnel_download_enabled, both on by default) and each answers
availability() with the live state, including the case where that download is
switched off. Checking PATH at boot would report a binary the adapter downloads
on demand as missing, and duplicate a better, later report. The same reasoning excludes nix, which
provisions inside the sandbox.
Nothing else is listed: there is no create_subprocess_shell under
src/synthorg/, and the devcontainer image build goes through aiodocker
rather than the docker CLI, so no docker entry can appear.
Construction-phase ordering invariants¶
agent_registrymust be built BEFOREauto_wire_meetings.auto_wire_meetingsbuilds the orchestrator with NO protocol registry. The factories bake in organisation-wide strategy policy read from settings, which do not exist at construction time, so themeeting_protocol_registrysubsystem installs them on the first reconcile pass (afterauto_wire_settingscomposes the resolver) and installs a replacement when that policy changes. A meeting run before that pass raisesMeetingProtocolNotFoundErrornaming the subsystem rather than running on boot defaults.tunnel_provideris wired unconditionally (not gated byintegrations.enabled).message_bus.set_quadratic_alert_sink(DispatcherQuadraticAlertSink(...))is called after the notification dispatcher is built, binding the in-memory bus's quadratic-fan-out enforcer to the dispatcher through theMessageBusprotocol seam (the NATS backend is a no-op). This is a protocol call, not anisinstance(InMemoryMessageBus)+ concrete-attr read.ConflictResolutionServiceis built and installed onCommunicationStateSlicebywire_conflict_resolution_service(api/_comms_conflict_wiring.py): the hierarchy comes from the boot company snapshot and the human resolver reuses the already-wired escalation store/processor/registry. The sharedLlmJudgeEvaluatoris built here too, from the provider that serves the pinnedCONFLICT_JUDGEmodel (resolve_feature_provider, not a naive first pick); a missing/non-serving provider leaves the judge unwired and the debate/hybrid resolvers fall back to authority._wire_meeting_conflict_bridgeruns AFTERwire_conflict_resolution_servicein the same pass (the bridge needs the built service) and AFTERauto_wire_meetings(it installs the bridge on the orchestrator viaset_conflict_escalation_hook). It also stores the bridge onCommunicationStateSlice.conflict_escalation_bridgeso the on-startup resolver rebind (_wire_resolver_dependents) can inject the persistence-backed resolver, making themeeting_conflict_escalation_enabledkill switch honour dashboard overrides. A no-op when either the orchestrator or the service is absent.PeerDiscoveryClientis built from the peer registry + SSRF network validator and placed onA2aStateSlice.peer_discoveryso the gateway'sskills/query/skills/negotiatehandlers can resolve learned peers.
On-startup ordering invariants¶
- Durable security state wires failure-tolerantly after persistence connects, inside
install_runtime_servicesviadurability_wiring.py:_try_wire_audit_chain_persistenceattaches aDurableAuditChainWriterto each liveAuditChainSink(hydrate + verify + drain). It is idempotent and degrades to in-memory-only on failure, logging the dedicatedAPI_AUDIT_CHAIN_PERSISTENCE_DEGRADEDevent. - The closed-loop
EvalLoopCoordinatoris built from the performance tracker + training service inwire_eval_loopand published onHrStateSlice; the periodicEvalLoopCycleSchedulerthat drivesrun_cycleis opt-in (hr.eval_loop_cycle_enabled) and re-readshr.eval_loop_cycle_pausedeach tick. - The durable hiring pipeline is its own
hiring_servicesubsystem:wire_hiringconstructs theHiringServiceover the registry, the approval store, the settings resolver and the provider catalogue, publishing it onHrStateSlice.hiring_service. The resolver and the catalogue are threaded in unresolved, because both are read when an approval is raised, so a provider configured after boot is offered on the next hire with no restart. Attachment is a hard prerequisite: it callsHiringService.attach_persistence(request_repo=persistence_of(app_state).hiring_requests)thenhydrate()so an approved hire survives a restart between approval and instantiation, and a failed attach aborts wiring rather than publishing a non-durable pipeline. Only hydration is isolated, so a hydration fault leaves in-flight requests for the next boot instead of refusing to start. A missing collaborator raisesSubsystemDeclinedErrornaming which, soGET /subsystemscan report it. _drain_resume_intentsruns LAST in_run_startup, after_wire_approval_gateand the worker-execution-service install hook, because a re-dispatch routes through both. It finishes any approval the previous process decided but died before resuming (see chat-inbound.md); non-fatal, so a drain fault leaves every marker for the next startup to retry rather than refusing to boot.-
The observability bridge config arm is applied alongside the notification dispatcher:
ObservabilityBridgeSettingsSubscriberwatches theobservability.*keys and recomposesget_observability_bridge_config()so console-level / telemetry-enabled edits take effect via the settings dispatcher. -
SettingsServiceauto-wire must precedeWorkflowExecutionObserverregistration, so it picks up the resolver-drivenmax_subworkflow_depthinstead of the seed default. OntologyServicewires afterpersistence.connect()via_wire_ontology_service.- Cost-dial services (
BudgetConfig,CostForecastRepository,BenchmarkScoreRepository, thebudget.benchmark_provider-selectedBenchmarkScoreProvider,CostForecaster,ParetoAnalyzer) wire via_try_wire_cost_dialAFTER persistence connects. It is failure-tolerant (logs anAPI_APP_STARTUPwarning and the controllers 503 if it fails or persistence is absent) and idempotent (skips when already wired), so a transient shared-app boot does not poison startup. The benchmark provider, repo, andParetoAnalyzer'sModelCapabilityMapare built inapi/_benchmark_wiring.py(stubdefault /measured;seed_benchmark_scoresboot-seeds the measured arm from the committedbenchmark_seed.json). The approved forecast'sforecast_id+ceiling_amountare stamped onto theTaskin the work pipeline's intake phase (WorkPipelineService._link_forecast) so the in-loopBudgetCheckerenforces the per-brief ceiling and the engine can stamp halt context for the resume banner. - The durable memory backend wires via
wire_memory_backend(inapi/lifecycle_helpers/memory_backend_wiring.py) AFTER persistence connects but BEFORE_install_runtime_services:_construct_agent_enginereadsMemoryStateSlice.backendeagerly, so a backend wired later would never reach an agent. It resolves the embedder (resolve_embedder_config) then buildscreate_memory_backendand connects; when no embedding model resolves it wires NO backend and logs at ERROR rather than silently falling back to the ephemeral keyword store (fail-loud). The same hook starts the consolidation scheduler (_wire_consolidation_scheduler), which is skipped when the interval isnever; org memory is its ownorg_memory_backendsubsystem, declared separately below. The embedder settings (memory.embedder_*) andmemory.backendare declared on thememory_backendsubsystem withrebuild_on_change=True: the backend bakes them in at connect time, so a write to one tears the instance down and builds a replacement rather than leaving stored vectors at an incomparable width. - Knowledge substrate wires via
wire_knowledge_engineAFTER persistence connects; failure-tolerant and ghost-wired (built regardless ofknowledge.enabled), gated only onhas_persistenceANDhas_memory_backend(logs anAPI_APP_STARTUPwarning and the knowledge controllers + MCP handlers 503 if either is absent), so a missing memory backend in dev does not poison startup.knowledge.enabled(Cat-1, default on, hot) is not a wiring gate; it is enforced live per request at the knowledge controllers + MCP handlers via the config resolver, so toggling it takes effect on the next request with no restart. EnvironmentService(per-project reproducible environments) wires in_install_runtime_servicesbehindhas_persistenceand is threaded intoAgentEngineExecutionServiceviabuild_runtime_services; the worker provisions ambiently (ActiveSandboxEnvironmentcontextvar) before the engine run, so a missing workspace logsENVIRONMENT_PROVISION_SKIPPEDrather than silently dropping the declared env.- Mid-flight steering splits its wiring in two by dependency: the steering INBOX (read path) is built from
persistence.project_brainand injected into the bootAgentEnginein the runtime-services step (persistence-only, memory-independentlist_currentprojection), while the steering SERVICE (write path) wires in_wire_steering_serviceAFTER_wire_project_brain(memory-gated brain) via partialapp_state.wire(CockpitStateSlice, ...)(NOTswap_slice, so the construction-phasesteering_notifierand the latersteering_servicecoexist on the slice). Wiring the service inside_wire_cockpit_serviceswould race the brain and 503 forever. - The red-team report repo is published on
SecurityStateSlice.red_team_reportsduring_install_runtime_services(decoupled from the review gate, via partialapp_state.wire), and_wire_deliverable_receiptsreads it so a receipt'sred_teamsection degrades to empty rather than erroring when the subsystem is off. agent_workspace_rootis resolved viaresolve_agent_workspace_root_env()INSIDE the_install_runtime_servicesstartup closure (its install guard runs it once), NOT at construction: the resolver fail-fasts on a non-absoluteSYNTHORG_DB_PATH, and that must not crash pure app construction (dev /:memory:/ OpenAPI export, which build the app without running startup). The resolvedPath | Noneis injected intoinstall_runtime_servicesrather than re-read from the environment inside it._apply_telemetry_db_layerruns immediately BEFOREtelemetry_collector.start. The collector is built pre-persistence (env > default), so this hook re-resolvestelemetry.enabledwith full DB > env > default precedence and callstelemetry_collector.apply_enabled(enabled=...)beforestart()reads it, so a DB/settingstoggle takes effect on restart. Failure-tolerant: aNoneresolver or a resolution failure logs anAPI_APP_STARTUPwarning and leaves the construction-time value untouched.- The construction-phase notification dispatcher is started by
_start_construction_dispatcherONLY when it is still the live dispatcher onNotificationsStateSlice. The bridge-config startup step (_apply_notification_dispatcher_config, run via_apply_bridge_config) may have rebuilt a replacement with DB-resolved timeouts, started it, and swapped it onto the slice, leaving the construction dispatcher orphaned; starting that orphan would open sinks nothing routes through and that on-shutdown (which closes the live slice dispatcher) never tears down. On a hot config-apply,_apply_notification_dispatcher_configstarts the new dispatcher BEFORE swapping it in (a failed start aborts the swap and keeps the running dispatcher, and aCancelledErroror non-critical failure mid-start closes the partially-started one), so no built-but-unstarted dispatcher silently drops events. - The JWT secret is resolved (
resolve_jwt_secret) and the dev-auth-bypass flag (resolve_dev_auth_bypass, envSYNTHORG_DEV_AUTH_BYPASS) BEFOREpersistence.migrate(), so a missing secret fails fast.SYNTHORG_DEV_AUTH_BYPASSis DEV-ONLY and defaultsFalse: when on it stampsAuthConfig.dev_auth_bypass=True, addsPOST /auth/dev-loginto the auth-exclude set, and emits a prominentAPI_APP_STARTUPsecurity warning. That endpoint mints a real admin session for the existing CEO with no password (404s when the flag is off, 401s when no admin exists, never creates one). It MUST never be enabled in production. The frontend dev bootstrap (web/.env'sVITE_DEV_AUTH_BYPASS, only honoured underimport.meta.env.DEV) calls it on load so the local Vite dashboard auto-logs-in.
Late-startup gated wiring hooks¶
Every hook below is declared in api/subsystems/registry.py and run by the
reconciler, not sequenced here; the notes record what each one needs and what
it degrades to. The requires line in the declaration is the authority. See
subsystem-reconciliation.md.
wire_quota_poller: gatedbudget.quota_poller_enabled+ connected persistence + wiredquota_tracker; appended AFTER_wire_features; idempotent; stopped inlifecycle_runner_shutdown.wire_risk_override_service: gated on aTieredTimeoutConfig+ persistence + scheduler; rebuilds the tiered policy's classifier wrapped inSecOpsRiskClassifierseeded from the durable override repo and swaps it viaApprovalTimeoutScheduler.set_timeout_policy; publishesRiskOverrideServiceonSecurityStateSlice; controllers + MCP 503 when unwired.wire_org_memory_backend: declared as theorg_memory_backendsubsystem (requirespersistence); failure-tolerantconnect(); publishesMemoryStateSlice.org_memory_backendconsumed by offboarding + ontology sync, degrading toNonewhen absent._wire_task_activity_observer: registers theTaskActivityObserveron theTaskEnginein the sametryblock as_wire_workflow_observer, gated on task engine + a liveChannelsPlugin(threaded viaMessageBusBridge.plugin) + a wiredHrStateSlice.performance_tracker; idempotent (checks_observersfor an existing instance). Publishes every persisted task transition toCHANNEL_TASKS(dashboard live-activity feed) and records aTaskMetricRecord(org health + completions sparkline) only for a classifiable, attributable executed terminal run (fresh into aTERMINAL_RUN_STATESmember, with a resolvedRunOutcomeand an assignee); a run whose outcome cannot be resolved (artifact-count fault) or that carries no assignee, and FSM-terminal-but-never-ran states (CANCELLED/REJECTED), are published but not recorded, so unclassifiable runs never skew health. LogsAPI_SERVICE_AUTO_WIREDon success andAPI_APP_STARTUPon each skip; a missing prerequisite disables the live feed rather than failing boot.wire_project_rollup_service: declared as theproject_rollup_servicesubsystem (requirespersistence + task engine); idempotent viaEngineStateSlice.project_rollup_service. RegistersProjectRollupService.on_task_state_changedas aTaskEngineobserver so a plan and its project advance from the tasks implementing them.TaskEnginere-reads_observersper mutation, so registering afterstart()is honoured. Failure-tolerant: a missing prerequisite leaves initiative status static rather than failing boot. The slice field is written only to serve the idempotency guard; the service runs off the observer, not slice resolution.attach_integration_stage/attach_evaluation_stage/attach_replan_trigger/attach_stall_escalation/attach_ship_retro_capture: the initiative tail, declared as five subsystems (initiative_integrate,initiative_evaluate,initiative_replan,initiative_stall_escalation,initiative_retro_capture), each separate from the rollup above and each requiring only what that one collaborator needs: the work pipeline, the provider + agent registries, the coordinator, the approval store, and both memory layers respectively. Each attaches onto the already-registered rollup, so the rollup keeps its early activation and its useful tailless work while the tail waits. Liveness is read one probe per collaborator (has_integration()/has_evaluation()/has_replan_trigger()/has_stall_escalation()/has_retro_capture()). The escalation is separate from the replan trigger rather than folded into it because it is what runs when the trigger is absent or refusing, so a boot with one must not read as covering the other. Splitting them off the rollup is what makes the tail reachable at all: the stages all decline during the rollup's own activation (the provider registry is built ~190 ms later), and the reconciler short-circuits on an already-active subsystem. Splitting them from each other is what makes the degradation table in initiative-tail.md true: one spec over the union of their requirements left a coordinator-less boot with no integrate stage either. The retro capture alone declares a teardown andrebuild_on_change, because the memory backends it holds are replaceable while the process runs._maybe_start_health_prober: the FIRST background service started in_start_background_services, gated on a wiredhealth_tracker+config_resolver(the provider-management service is optional and only supplies the SSRF discovery policy). It builds theProviderHealthProber,start()s it, and only then hands it back toProviderManagementService.set_probe_requesterso a provider created after boot is probed on write instead of sittingunknownuntil the next cycle (providers.health_probe_interval_seconds, re-read per cycle). The hand-off is deliberately afterstart(): a prober that never started must not become the probe requester. Failure-tolerant (critical errors re-raise, everything else logs anAPI_APP_STARTUPwarning and returnsNone), so a prober fault leaves health derived from real call outcomes rather than refusing to boot;lifecycle_runner_shutdownstops it.HealthMonitoringPipeline(judge + triage + dispatcher) is built inbuild_runtime_servicesand threaded intoAgentEngineExecutionService(health_pipeline=/health_enabled=), re-readingengine.health_monitoring_enabledper run; absent dispatcher means no pipeline.
Controller-facing read services¶
ConversationalResumeServicelives inmeta/chief_of_staff/(NOTapi/services/, so the earlyapi_core_stateimport chain never pulls thecommunicationenum modules and trips the cold-import cycle gate), is published onMetaStateSliceby_wire_conversational_repositories_and_reconcile, and is deliberately UNGATED (wraps only the persistence repos, never the toggle-gated Chief-of-Staff feature services) so a decided conversational approval still resolves after its feature switches off.- The single
WorkflowExecutionService(_wire_workflow_observer) caches theconfig_resolverand resolvesmax_subworkflow_depthper call so settings hot-reload is preserved (constructing with a resolved int would freeze the seed default; aNoneresolver logs a warning and falls back toEngineBridgeConfig().max_subworkflow_depth). _wire_webhook_request_serviceswireswebhook_replay_protectorunconditionally andwebhook_activity_serviceonly on connected persistence. Idempotency keys on mutating endpoints MUST bind the resource id (f"{resource_id}:{idempotency_key}") so a reused key cannot collide across resources.
Self-extending toolkit (toolsmith)¶
Autonomous detection wires in wire_toolsmith AFTER install_dynamic_tool_layer (gated on self_improvement.tool_creation_enabled + provider + connected persistence): install_capability_gap_sink(runtime.service) routes every capability_gap MCP envelope into the gap store (fire-and-forget; write failure logs via safe_error_description, never an unhandled task-exception traceback), and a ToolsmithCycleScheduler (cadence toolsmith.cycle_interval_seconds, floor 60s) drives run_cycle periodically with a meta.toolsmith_cycle_paused kill-switch (re-read each tick, fail-safe to enabled). The ToolsmithStateSlice carries service + cycle_scheduler as a both-or-neither paired invariant; lifecycle_runner_shutdown stops the scheduler. The cycle only PROPOSES (human approval still gates apply).
Periodic model-refresh¶
Wires in wire_model_refresh (_wire_model_refresh, AFTER wire_toolsmith), gated on providers.model_refresh_mode != off AND a built provider-management service AND connected persistence (off-by-default, so a normal boot skips it). The cadence modes (detect_only / reconcile_recommend) build + start() a ModelRefreshScheduler BEFORE publishing ModelRefreshStateSlice and roll back (no publish) if start fails; manual_only wires the on-demand service with no scheduler. The slice's both-or-neither invariants: a scheduler implies a service, and a service implies a recommendation_repo. lifecycle_runner_shutdown stops the scheduler. The scheduler re-reads the live mode + auto-apply flag each tick (fail-safe to off), and stop() does NOT reset its lifecycle lock/event to None (it passes reset_primitives_on_stop=False to the shared AsyncCycleScheduler base: clearing them under the held lock opens a rebind race).
Runtime tool-call failure feedback¶
Wires in wire_tool_call_feedback (AFTER _wire_model_refresh), gated on a built provider-management service AND connected persistence (NOT on providers.tool_call_feedback_enabled: the ToolCallFeedbackTracker re-reads that setting live per observation so the feature toggles without a restart while the cheap sink stays installed). Failure-tolerant + idempotent: a missing resolver / management / persistence logs an API_APP_STARTUP warning and returns (never an invisible skip). The tracker is published on ToolCallFeedbackStateSlice, then install_tool_call_signal_sink(tracker) runs LAST so a partial build leaves no dangling sink; lifecycle_runner_shutdown uninstalls the sink + clears the slice. The tracker keeps its instance lock OFF the capability-writer call (the writer takes ProviderManagementService._lock), so the two locks never nest. The matcher hard-fails every agent on tool_calls_verified is False (authoritative over the optimistic supports_tools path, and unconditional because tool calling is a floor rather than a per-role requirement); a genuine tool-call success only re-enables a truly-downgraded (False) model, never promotes an untested (None) one.
Stakes-gated red-team completion¶
ReviewGateService reads the red-team threshold off the shared CapabilityPolicy per decision (set_capability_policy, wired in startup_steps.py right after the deliverable-input builder), so a write to engine.red_team_min_stakes takes effect on the next completion rather than at the next restart; with no policy wired it falls back to the declared default (HIGH). The gate fires only for task.stakes >= red_team_min_stakes; a below-threshold completion logs RED_TEAM_GATE_SKIPPED and dispatch_completion runs it INLINE (a missing task also runs inline so complete_review raises TaskNotFoundError synchronously) rather than deferring a no-op to the background.
Self-improvement rollback executor¶
Wires in api/lifecycle_helpers/meta_apply_wiring.py::_wire (failure-tolerant + idempotent, off by default with the self-improvement feature): _build_rollback_executor constructs the six mutators (config/prompt/principle-removal/architecture/code unconditional, branch gated) and threads the executor into SelfImprovementService, which dispatches the applier-MATERIALISED inverse operations (carried on RolloutResult.applied_rollback_operations, NOT the proposal's static plan) on a post-rollout regression, flipping REGRESSED to ROLLED_BACK. The branch/revert_branch handler is gated on code_modification.github_token + github_repo (absent means branch revert omitted, matching the applier's credential gating); github_api_url is https-validated (token rides the Authorization header). A materialised op with no registered handler logs ERROR reason="unregistered_operation_type" and keeps REGRESSED (never a silent swallow).
Runtime services selection¶
synthorg.workers.runtime_builder.build_runtime_services selects behind ONE provider-present switch and returns a RuntimeServices pair (worker execution service + multi-agent coordinator) built from a SINGLE shared boot AgentEngine: AgentEngineExecutionService + a build_coordinator(...) coordinator with a provider, or NoProviderExecutionService + None coordinator as the empty-company backstop. The provider-present arm validates coordination.decomposition_model is non-blank before building the coordinator and raises CoordinationConfigError at boot when it is empty (the setting ships blank, so an operator must set a model id from their catalogue). The _install_runtime_services boot hook installs both via the AppState.worker_execution_service and AppState.coordinator seams; it is appended FIRST after the persistence/SettingsService hooks so the once-only set_worker_execution_service and if-absent set_coordinator_if_absent seams cannot lose the race with the worker property's lazy LifecycleAdvancingExecutionService default. Empty-company rejects task creation at the controller (AgentRuntimeNotConfiguredError, 4014) and /coordinate honestly 503s (no coordinator). swap_worker_execution_service / swap_coordinator / swap_provider_registry hold a lock (synchronised against lazy reads).
Setup completion¶
post_setup_reinit() (provider reload, agent bootstrap, AND runtime-services rebuild + dual hot-swap of the worker execution service and coordinator, defined in src/synthorg/api/controllers/setup/_runtime_wiring.py) propagates failures, and settings_svc.set("api", "setup_complete", "true") only runs if reinit returns clean. The whole check/validate/reinit/persist sequence is serialised under COMPLETE_LOCK (also in _runtime_wiring.py) so two concurrent /setup/complete requests cannot race on the flag write. A half-configured runtime presenting itself as "complete" is worse than a clear error the operator can retry after fixing the underlying provider config.