Handles

Read this article for background on Handle Paths: link.

One of the fundamental problems in software is identity. Objects exist on the heap, but the heap is transient. When a device reboots, everything is destroyed and rebuilt. There needs to be a reliable way to refer to “the same” logical object through these changes. In KOS, we solve this problem using handle paths, which provide a deterministic,universal, and human-readable identity for objects. Handle paths allow us to reliably locate equivalent objects across restarts.

Handle paths are used regularly throughout KOS.

Controllers: When making an HTTP request to trigger an operation, such as testing a piece of hardware like a sensor board, the request can include a handle path as a parameter. The controller uses that path to locate the exact object and invoke the requested behavior: link.

Configuration: All configs in KOS are identified by handle paths as well, which ensures that they are unique and persistent across reboots. Handle paths allow configs to be viewed and edited through the “Configuration Viewer” tool on KOS Studio, and overridden via config.xml files without any ambiguity about which object is being configured: link

Logging: Handle paths are also designed to be human readable, which makes them invaluable for logging and debugging. When a path appears in logs, it immediately tells you which object was involved, making it much easier to trace behavior through a complex system.

Databases: For the readability and universality, handle paths are often stored in databases instead of object references, allowing persisted data to be mapped back to live objects when the system restarts.

HandleAware and Path Construction

Any object that implements HandleAware can have a handle path. Such objects expose a Handle property and are responsible for defining how the handle is constructed, typically by overriding getHandle(). A handle is not created in isolation. Instead, it is part of a chain, where each object declares a parent, that parent declares its parent, and so on, until a root is reached. The resulting handle path is formed by walking from the root down to the target object.

Because paths are constructed this way, they naturally describe the structure of the system and remain readable. For example, a path such as assembly:core:board:rfid:antenna:static describes exactly where the board exists within the larger hierarchy.

Roots and Determinism

Every handle path must eventually chain back to a root. In KOS, roots are managed by the HandleService, which is responsible for creating and tracking handles and their associated objects. If an object never reaches a root, HandleService is never aware of it, and no handle is created. In that case, calling getHandle() will return null.

KOS provides a small set of default roots. Core services like ConfigService and ApplicationService live under the kos root because they need to behave consistently, no matter which device they run on. Many application-level services that should also remain consistent across devices use the system root. In both cases, the root ensures that the logical identity of these objects stays the same regardless of the underlying hardware.

Some objects, however, are inherently device-specific. Hardware assemblies and boards fall into this category, and their handles are rooted under assembly.

These three roots, kos, system, and assembly, cover the vast majority of use cases. While it is technically possible to create custom roots using HandleService, this is rarely necessary.

Objects within a Context

Objects that live within a context often do not need to manually manage their handle chaining. When an object implements ContextHandleAware and is created inside a standard KOS context, such as the core context, system application context, or an application context, its handle is created automatically.

Objects outside a Context

Objects that are outside any context need to be manually chained back to a root. If they aren’t,HandleService never sees them, and getHandle() will return null. You can link such objects to an existing root manually, or create a custom root for them using HandleService. This is important to remember for any standalone objects that need persistent identities.

Names, Prefixes, and Disambiguation

Each handle in a path has a name that identifies the object relative to its parent. In addition to the name, a handle may also include an optional prefix. Prefixes exist to prevent collisions when different types of objects share the same logical name.

For example, imagine a control board that has two pieces of hardware, both named water one is a pump, and the other is an RFID antenna. If we only used the name water, both paths would resolve to board:water, and we wouldn’t be able to tell them apart. Prefixes solve this by encoding the type of the object. The pump uses the prefix pump and the RFID antenna uses rfid, giving two distinct paths: board.pump:water and board.rfid:water. In this way, the prefix identifies the type of object, while the name identifies the specific instance.

Handle Paths as a Graph

Taken together, all handle paths form a graph that maps paths to objects. HandleService maintains this mapping and allows objects to be resolved by path when needed. In most cases, this lookup happens implicitly through KOS mechanisms, and direct access is unnecessary.

There are situations, however, where explicit lookup is useful. For example, if a handle path is stored in a database, it can be used to retrieve the associated object via HandleService.getBean(path) and perform the required operation.