Skip to main content

Loc.ai:Link Changelog

All notable changes migrating from locai-link-old to locai-link-new.

Loc.ai:Link has evolved into a single-process, pipeline-based architecture that replaces the older monolithic system, improving efficiency and maintainability. It now features a plugin-driven design with typed configuration, enabling flexible integration of AI models and components. With OTA updates, session recovery, and service deployment, it is a fully modular, production-ready agent framework.

Architecture

A modular, pipeline-based runtime. The control plane (lifecycle, configuration) is separated from the data plane (inference, telemetry) for performance and resilience.

Changed

  • Single-process model: The previous two-process split (manager.py supervisor + agent.py worker) has been consolidated into a single main.py. OTA updates now use os.execv() to replace the process in place rather than relying on a parent supervisor loop — the PID is preserved across updates.

  • Pipeline-based runtime: The monolithic agent.py (1,496 lines), which handled command polling, inference dispatch, serving, and metrics, has been replaced by AgentRuntime + Pipeline threads. Each pipeline is a Source → Sink pair running on its own thread, composable from config.

  • Component registry: Components (HTTP sources, Zenoh sinks, system monitors, command handlers) self-register via a @ComponentRegistry.register("name") decorator and are instantiated from declarative config.

  • Pydantic config models: Ad-hoc JSON config handling has been replaced with typed AgentConfig, PipelineConfig, TransportConfig, and GenericConfig Pydantic models. Schema version is pinned at 2.1.

  • Session state persistence: A new StateManager writes timestamped configs/session_*.json files for crash recovery. The agent automatically resumes the latest session on restart, and any running pipelines are re-started accordingly.

Removed

  • manager.py (1,080 lines) subcommands have been folded into main.py.
  • src/link/serving/ LLMServer, WhisperServer, and BaseServer moved into plugins.
  • src/link/inference/ dispatcher.py and TFLite runners (language_model_gguf.py, image_detection_cpy_tflite.py, audio_classification_yamnet_tflite.py) replaced by plugin adapters.
  • src/link/logger/ custom module replaced by src/link/utils/logger.py with structured async handlers.
  • src/link/analytics.py analytics now flow through the generic reporting handler system.
  • src/link/components/buffers.py unused LocalBuffer stub removed.

Registration & Onboarding

Registration & Onboarding now supports flexible identity resolution and simplified device activation using keys. Authentication is updated to JWT-based login with email/token, and passwords are handled securely via prompt input.

Added

  • Four-tier identity resolution in main.py run explicit --config, auto-resume of the latest session, just-in-time onboarding with --registration-key, or factory defaults.
  • activate_device() for re-activating existing devices using only --device-id and --registration-key.

Changed

  • The register_device() function previously accepted --username and sent it in the request body. It now accepts --email (or a pre-obtained --token) and uses login_and_get_token() to obtain a JWT, which is sent as Authorization: Bearer on the /devices/register-with-key request matching the backend's expected auth flow.
  • Passwords are prompted securely via getpass when omitted from the command line.

Installation

Installation is streamlined with one-liner scripts for Linux, macOS, and Windows, enabling quick setup across platforms. The new main.py install command automates the full flow clone, setup, register, and run in a single step. Setup is now managed via main.py setup, replacing the old manager-based approach.

Added

  • One-liner install scripts for Linux/macOS (install.sh), Windows PowerShell (install.ps1), and Windows CMD (install.cmd). Each script bootstraps uv, detects local vs. remote main.py, and hands off to the new install subcommand.
  • Main.py install subcommand orchestrates the full flow clone/update repo → setup → register → run, in a single command.

Changed

  • Setup is now handled via main.py setup (with --dev and --tui extras) instead of manager.py setup --extras.

Plugins

The system now uses a plugin-based architecture, where each capability (LLM, audio, vision) is packaged as an independent, installable module. Core plugins like language_model, audio_transcriber, and classifiers enable flexible AI workloads with support for CUDA, ARM64, and optimized builds.

Enhancements like caching, improved loading, and smarter configuration make plugins more efficient, scalable, and easier to manage.

Added

  • Plugin architecture: Plugins are standalone installable packages that register via the locai.plugins entry point. Each plugin has its own pyproject.toml, adapter.py, and install.py.
  • language_model: Plugin ports the LLM server logic (llama-server lifecycle). Pinned to llama.cpp b8808.
  • audio_transcriber: Plugin ports the Whisper server logic (whisper-server lifecycle). Pinned to whisper.cpp v1.8.4.
  • image_classifier: Plugin-vision inference via TFLite.
  • audio_classifier: Plugin-audio tagging via TFLite.
  • CUDA build-from-source fallback: On Linux when the CUDA toolkit (nvcc) is detected — enables -DGGML_CUDA=ON for optimal GPU performance.
  • Tag-based caching: Each plugin install uses a tag file to skip re-download/rebuild when already at the pinned version. Re-running install.py after an OTA update is lightweight.
  • ARM64 Linux support: For llama.cpp prebuilts (the old code was x64-only).
  • macOS quarantine stripping: xattr -dr com.apple.quarantine is applied after extraction to prevent Gatekeeper from blocking binaries.
  • Symlink preservation: When extracting tarballs versioned shared library names (e.g., libmtmd.0.dylib) now resolve correctly.

Changed

  • Removed hardcoded --chat-template chatml the language_model plugin now only passes --chat-template when explicitly configured, allowing llama.cpp to auto-detect from the model's metadata.
  • Health-check timeout raised from 30s to 120s for plugin servers — large models on CPU can take longer to load.
  • LD_LIBRARY_PATH now walks subdirectories to locate ggml*.so / whisper*.so files, as CUDA shared libs may reside in nested folders.

Configuration

Configuration is now fully declarative and flexible, supporting pipelines defined via config with dynamic source sink mapping. It introduces Zenoh as an alternative transport alongside HTTP, enabling more scalable communication. Logging and reporting are config-driven with template-based customization, allowing reusable and device-specific configurations.

Added

  • Zenoh transport as an alternative to HTTP for the control plane and pipelines. Set TransportConfig.type = "zenoh" or "http".
  • Declarative pipelines in config pipelines: [{id, source, sink, active}]. Sources and sinks are instantiated by name from the component registry.
  • Config-driven logging and reporting handlers LoggingConfig.handlers and ReportingConfig.handlers accept a list of typed handler configs (console, http, zenoh).
  • Template substitution in handler args ${identity.device_id}, {cid}, and {mid} are resolved at runtime, allowing a single config to serve multiple devices.

Pipelines

Pipelines now treat empty inputs as successful no-ops, preventing false failure warnings during idle polling. This improves stability by ensuring the pipeline loop handles inactive states correctly without unnecessary alerts.

Changed

  • AgentCommand sink now returns True on empty input (previously returned None). The pipeline loop treats non-truthy sink results as failures and warns accordingly. Idle poll ticks (http_poll returning [] when no commands are pending) previously triggered a spurious "Sink is returning False" warning. An empty dispatch is now treated as a successful no-op.

Transport, Logging & Reporting

Transport, Logging & Reporting now use a structured, async system with LinkReporter and non-blocking handlers for HTTP and Zenoh. Error handling is improved with retry logic, clear classification (retryable vs fatal), and standardized log formats, ensuring reliable and scalable observability.

Added

  • LinkReporter a custom logger class exposing report_lifecycle(status), report_command(cmd_id, status, output), and report_model(...) for structured status reporting.

  • AsyncHandler base class with a worker thread and queue — all handlers are non-blocking. Subclasses include:

    • AsyncHTTPHandler routes events to HTTP endpoints via template lookup (PUT for lifecycle_status, POST for everything else).
    • AsyncZenohHandler publishes to Zenoh topics.
  • HttpError exception class with status, reason, and retryable fields allows callers to distinguish transient failures (timeout, 5xx, connection refused) from non-retryable ones (401, 403, 404). The previous HttpClient silently swallowed all errors as None/False.

  • agent_version in lifecycle status payload report_lifecycle() now includes the agent's installed version (read from importlib.metadata.version("locai-link")), satisfying the backend's AgentStatusUpdate.agent_version semver requirement.

Changed

  • HttpClient.get() / post() now classifies errors: timeouts, 5xx responses, and connection errors return None/False (retryable); 4xx auth/client errors raise HttpError. HttpPoller and HttpPublisher catch HttpError and log with actionable context before re-raising.

  • HTTP log payload shape now matches the backend's LogCreate schema: {message, severity, category}. Severity is lowercase (DEBUG maps to "info"); category defaults to "other" and can be set via logger.info("msg", extra={"category": "security"}). This replaces the previous {timestamp, level, message, logger} shape.

  • AsyncHTTPHandler now retries on timeouts, connection errors, and 5xx responses with exponential backoff (0.5s, 1.5s, capped at 2 retries). 4xx responses remain fatal and are not retried. Timeout is configurable per handler via args.timeout (default 10s), split into (connect=3s, read=timeout) fast-fail on unreachable hosts, tolerant on slow responses.

  • HttpClient.get() timeout demoted from WARNING to DEBUG polling is self-healing (the next tick retries), and a flaky network was generating excessive console noise at warning level.

Service Deployment

Service Deployment now includes a cross-platform service manager, enabling the agent to run as a native service on Linux, macOS, and Windows. It supports production mode execution and graceful shutdown, simplifying deployment and lifecycle management.

Added

  • Cross-platform service manager (src/link/infra/service.py) — the ServiceManager factory selects the appropriate backend:
    • Linux → systemd user service at ~/.config/systemd/user/locai-link.service
    • macOS → LaunchAgent plist at ~/Library/LaunchAgents/io.locai.locai-link.plist
    • Windows → Windows Service via sc.exe (requires admin privileges)
  • main.py run --prod installs and starts the agent as an OS service.
  • main.py stop gracefully stops the agent (and zenohd, if installed).

OTA Updates

OTA Updates now support seamless, zero-downtime upgrades using in-place restarts via os.execv(), preserving the running process. Updates are branch-aware, stash-safe, and intelligently refresh only the plugins actually in use.

The new updater system ensures clean pipeline shutdown, automated installs, and efficient version management without needing an external supervisor.

Added

  • UPDATE_AGENT command handled by AgentRuntime: On receipt reports completion, cleanly shuts down pipelines, and signals main.py to update.
  • src/link/app/updater.py: Provides pull_and_update() (git fetch/stash/pull/pop + uv pip install -e .), reinstall_plugin_binaries() (config-driven; see Changed below), get_current_branch(), and get_local_version().
  • In-place restart via os.execv(): The process image is replaced while preserving the PID. systemd/launchd see a continuously running process with no downtime gap.
  • Branch-aware updates: Dev branches pull from origin/<current-branch>, not origin/main.
  • Stash-safe updates: Dirty working trees are stashed and reapplied around the pull.
  • -DGGML_NATIVE=OFF on macOS: For both llama.cpp and whisper.cpp builds avoids ggml's -mcpu=native fallback, which AppleClang rejects on arm64. Metal + Accelerate handle performance-critical paths on Apple Silicon with no throughput regression. Linux and Windows are unchanged.
  • Silenced detached-HEAD git advisory: Tagged clones now pass -c advice.detachedHead=false inline to suppress cosmetic noise from OTA build logs.

Changed

  • Reinstall_plugin_binaries() is now config-driven. Previously, every plugin under plugins/ had its install.py re-run on every OTA — so a device running only language_model would still attempt to build whisper.cpp, TFLite, etc. The updater now walks the active
  • AgentConfig.pipelines[*].source/sink.type, maps each type to its owning plugin via that plugin's [project.entry-points."locai.plugins"] in pyproject.toml, and only refreshes plugins that are actually referenced. Unused plugins are silently skipped.

Removed

  • The old EXIT_CODE_UPDATE = 42 + subprocess-loop supervisor in manager.py. The new architecture requires no external supervisor.

Testing & CI

Testing & CI now includes 77+ unit tests and full integration tests across plugins, ensuring reliability of core features and model workflows. It also introduces a multi-OS CI pipeline with version checks, improving code quality, consistency, and release control.

Added

  • 77 unit tests: Covering HTTP client error classification, onboarding auth flow, state manager version handling, OTA updater logic, runtime command handling, service manager across all three oses, zenoh router, config loading, and platform detection.
  • ci pytest marker:For tests requiring external binaries or network access skipped locally by default, enabled in CI via -m "" override.
  • Integration tests:In each plugin directory download real models, spawn real server binaries, and verify full transcription/completion flows.
  • Multi-OS CI matrix: (Ubuntu, macOS, Windows) for both unit and integration jobs.
  • Version-bump gate on PRs: Fails if the pyproject.toml version has not been incremented.
  • audio_transcriber wired into the integration-test job alongside the other three plugins.

CLI Reference

The CLI has been simplified by replacing manager.py with a unified main.py, consolidating all commands into a single entry point. It now supports setup, install, run, stop, and plugin management, making the workflow cleaner and easier to use.

Before (old manager.py)

manager.py install        # Full installation wizard
manager.py setup # Configure venv and deps
manager.py reset # Clean up artifacts
manager.py register # Register device
manager.py activate # Activate a pre-registered device
manager.py update # Pull latest code
manager.py run # Run agent (supervisor loop)
manager.py install-deps # Install llama/whisper server binaries

After (new main.py)

main.py setup             # Install Python dependencies (--dev, --tui)
main.py install # Full installation wizard
main.py run # Run agent (in-process, handles OTA via execv)
main.py stop # Stop all services
main.py reset # Clean up environment (--hard)
main.py install-plugin # Install a plugin by name
main.py tui # Launch text UI (optional)

Breaking Changes

Breaking Changes introduce updates like --email replacing --username, a new config schema (v2.1), and removal of manager.py in favor of main.py. Plugins must now be installed separately, and logging/reporting formats have been standardized through the new LinkReporter system.

info

Review these carefully before migrating.

  • Registration argument is --email (not --username).
  • Config schema version is 2.1. Earlier state files are rejected and will not be loaded.
  • manager.py no longer exists; all commands must be run via main.py.
  • Plugins must be installed separately as editable packages (uv pip install -e "plugins/<name>"). They are not bundled with the core agent.
  • Agent status, command status, and model status payloads now flow through the new LinkReporter handler system direct requests.post calls to /agent/{device_id}/status have been removed.
  • HTTP log payload shape changed from {timestamp, level, message, logger} to {message, severity, category} to match the backend's LogCreate schema. Backends consuming /logs must accept the new shape; the old field names are no longer emitted.

You can refer to the locai-link Github repositary.