# Pulse Docs — full corpus Generated for AI-agent consumption. Each page below is preceded by its slug and title. --- slug: /pulse/index title: Home source: pulse --- # Pulse documentation ## What is Pulse? Pulse is a technology platform and execution paradigm for building, deploying, and operating realtime production systems at scale. It integrates control, content, analysis, and infrastructure to orchestrate live reactive and interactive experiences. Its hybrid cloud/on-prem infrastructure is built for scalability and web accessibility, making control, visualization, viewing, and monitoring accessible from anywhere. ## Why does Pulse exist? We believe that: - The technical infrastructure that supports immersive experiences should be straightforward to set up, robust, and re-usable. - Designing immersive experiences should feel intuitive and unburdened by the cost of technical change. - Working in realtime, all the time, accelerates iteration — the faster we iterate, the more freely we explore creative possibility space and refine artistic vision. Our primary goal is to empower creators in bringing ambitious concepts to life. We do so by building a platform that is modular, composable, and extensible, can scale to meet the performance demands of large reactive experiences, and is flexible enough to handle complex and dynamic emergent narratives. ## How does Pulse work? Pulse supports the process of transforming creative vision into live experience. Content and analysis engines handle inputs, processing, and outputs. Rocketship declares event relationships and routes events between engines. Creators see the results live and refine the experience in realtime. --- slug: /pulse/onboarding title: Access and setup source: pulse --- # Access and setup ## Day one - [PlasticSCM](https://www.plasticscm.com/) org access for project repos - [GitHub](https://github.com/ignition-is-go) org access for code repos - PulseRemoteDesktop (custom [RustDesk](https://rustdesk.com) client) for accessing Pulse VMs - Lucid domain email (@lucid.rocks) for account integrations - [Slack](https://slack.com) - [Discord](https://discord.com) - [Dropbox](https://www.dropbox.com) - [Miro](https://miro.com) ## Technical Director Production systems, deployment planning, and coordination across tools. - [Platform overview](/pulse/index) for product responsibilities. - [Pulse Cluster](/pulse-cluster/overview) for render-fleet state, recipes, and lifecycle operations. - [Pulse Deploy](/pulse-deploy/overview) for infrastructure provisioning and configuration. - [Levi](/levi/readme) for tracking repository work and dependencies. ## Unreal Engine Developer / Technical Artist Plugin features, integration tests, lookdev, and content-system development. - [Pulse Unreal](/pulse-unreal/readme) for the plugin suite and content systems. - [RshipExec](/pulse-unreal/plugins/rshipexec/readme) for connecting Unreal Engine to Rocketship. - [Pipe](/pulse-pipe/docs/24-user-guide) for asset publishing and the Unreal connector. ## TouchDesigner Developer / Artist Media playback and lighting control via TouchDesigner. - [Rship TouchDesigner](/rship-touchdesigner/readme) for exposing TouchDesigner and Notch controls to Rocketship. - [Rocketship core terms](/rship/core-concepts/terms) for targets, actions, and emitters. - [Rocketship SDKs](/rocketship-sdks/overview) when developing custom applications or executors. ## Rship Programmer / Developer Scene logic, show programming, and application or framework development. - [Rocketship](/rship/get-started/introduction) for reactive control and executor connections. - [Rocketship SDKs](/rocketship-sdks/overview) for language and host-application SDKs. - [Myko](/myko/readme) and [Hyphae](/hyphae/hyphae/readme) when changing event-sourcing or reactive dataflow code. - [Phosphor](/phosphor/readme) for rendering Rocketship media scenes. ## AI Developer Camera and vision workflows, music analysis, and model development. - [Optik](/optik/readme) for camera calibration, tracking, and video inference. - [Tactus](/tactus/readme) for music analysis and publishing results as Rocketship event tracks. - [Rocketship SDKs](/rocketship-sdks/overview) when connecting analysis results to control applications. ## Asset Artist / Pipeline Developer Asset publishing across Houdini, Substance Painter, and Unreal Engine. - [Pipe](/pulse-pipe/docs/24-user-guide) for versioned publishes, bundles, and provenance. - [Pulse Unreal](/pulse-unreal/readme) when working with the Unreal plugin suite. ## Infrastructure Engineer / Operator Infrastructure provisioning, configuration, and fleet operations. - [Pulse Deploy automation](/pulse-deploy/automation) for provisioning and control-plane responsibilities. - [Pulse Cluster](/pulse-cluster/overview) for operating the render fleet. - [Pulse Deploy operations](/pulse-deploy/operations) for infrastructure convergence, diagnostics, and repair. - [Reference deployment](/pulse-deploy/reference-deployment) for the documented infrastructure layout. ## Agent Developer / Operator Remote agent sessions, task tracking, and coordination. - [Pulse Agents](/pulse-agents/overview) for launching and managing durable remote sessions. - [Marshal](/marshal/readme) for live agent presence and messaging. - [Levi](/levi/readme) for repository tasks and dependencies. - [Myko MCP](/myko-mcp/overview) for discovering and accessing application capabilities through MCP. For source-code work in any role, use [Pulse CLI workspaces](/pulse-cli/workspaces) and the [repository directory](/pulse/repository-directory). --- slug: /pulse/repository-directory title: Repository directory source: pulse --- # Repository directory Find the code behind a Pulse product or library. Repository names link to GitHub. The documentation column links to a guide on this site where available, or to the source repository. Access to private repositories requires a GitHub account with the appropriate organization access. The [Pulse workspace manifest](https://github.com/ignition-is-go/pulse/blob/main/pulse.toml) lists the 45 managed repositories. They are grouped here by primary role. Cross-cutting projects can support products in several categories. Rocketship's language SDKs live inside `rship`, not in separate repositories. [Rocketship SDKs](/rocketship-sdks/overview) also links to RshipExec in `pulse-unreal` and the TouchDesigner SDK in `rship-touchdesigner`. [Myko MCP](/myko-mcp/overview) is part of `myko`. ## Production | Repository | Responsibility | Documentation | | --- | --- | --- | | [gst-rs](https://github.com/ignition-is-go/gst-rs) | GStreamer graphs, asynchronous media primitives, and SDI I/O. | [Browse source](https://github.com/ignition-is-go/gst-rs) | | [nmos-rs](https://github.com/ignition-is-go/nmos-rs) | NMOS protocol entities, registration, node services, and UI. | [Browse source](https://github.com/ignition-is-go/nmos-rs) | | [optik](https://github.com/ignition-is-go/optik) | Camera calibration, control, vision inference, and live video. | [Optik](/optik/readme) | | [phosphor](https://github.com/ignition-is-go/phosphor) | Vulkan media rendering controlled by Rocketship output intent. | [Phosphor](/phosphor/readme) | | [pixelstream-recorder](https://github.com/ignition-is-go/pixelstream-recorder) | Pixel Streaming track capture, transcoding, and recording delivery. | [Browse source](https://github.com/ignition-is-go/pixelstream-recorder) | | [pixelstreaming-infrastructure](https://github.com/ignition-is-go/PixelStreamingInfrastructure) | Signalling, SFU, and frontend infrastructure forked from Epic. | [Browse source](https://github.com/ignition-is-go/PixelStreamingInfrastructure) | | [program-compositor](https://github.com/ignition-is-go/program-compositor) | Combines Unreal video and Tactus server audio into one streamed program. | [Browse source](https://github.com/ignition-is-go/program-compositor) | | [pulse-pipe](https://github.com/ignition-is-go/pulse-pipe) | Versioned asset publishing, immutable bundles, and DCC connectors for Pipe. | [Pipe](/pulse-pipe/docs/24-user-guide) | | [pulse-pixelstream](https://github.com/ignition-is-go/pulse-pixelstream) | Collaborative Unreal Pixel Streaming viewing, presence, and shared control. | [Browse source](https://github.com/ignition-is-go/pulse-pixelstream) | | [pulse-unreal](https://github.com/ignition-is-go/pulse-unreal) | Pulse Unreal plugin suite, including RshipExec and content systems. | [Pulse Unreal](/pulse-unreal/readme) | | [rivermax-rs](https://github.com/ignition-is-go/rivermax-rs) | Rust bindings and wrappers for NVIDIA Rivermax. | [Browse source](https://github.com/ignition-is-go/rivermax-rs) | | [rship-reaper](https://github.com/ignition-is-go/rship-reaper) | REAPER playback and transport executor for Rocketship. | [Browse source](https://github.com/ignition-is-go/rship-reaper) | | [rship-touchdesigner](https://github.com/ignition-is-go/rship-touchdesigner) | Rocketship SDK and executor integration for TouchDesigner and Notch. | [Rship TouchDesigner](/rship-touchdesigner/readme) | | [rship](https://github.com/ignition-is-go/rship) | Rocketship control, entities, and language SDKs. Tracks `dev`. | [Rocketship](/rship/get-started/introduction) | | [st2110-rs](https://github.com/ignition-is-go/st2110-rs) | SMPTE ST 2110 media-transport stack. | [Browse source](https://github.com/ignition-is-go/st2110-rs) | | [tactus](https://github.com/ignition-is-go/tactus) | Music analysis and playback services with Rocketship event-track output. | [Tactus](/tactus/readme) | | [unreal-rc](https://github.com/ignition-is-go/unreal-rc) | Rust HTTP and WebSocket client for Unreal Remote Control. | [Browse source](https://github.com/ignition-is-go/unreal-rc) | ## Operations | Repository | Responsibility | Documentation | | --- | --- | --- | | [agent-kit](https://github.com/ignition-is-go/agent-kit) | Canonical agent skills and MCP configuration rendered for supported harnesses. | [Browse source](https://github.com/ignition-is-go/agent-kit) | | [harness](https://github.com/ignition-is-go/harness) | Reliability and workflow primitives for stochastic, expensive, or remote calls. | [Browse source](https://github.com/ignition-is-go/harness) | | [levi](https://github.com/ignition-is-go/levi) | Git-aware task tracking, dependencies, claims, and completion. | [Levi](/levi/readme) | | [marshal](https://github.com/ignition-is-go/marshal) | Live agent-session presence and messaging across harnesses and machines. | [Marshal](/marshal/readme) | | [pulse-agents](https://github.com/ignition-is-go/pulse-agents) | Durable remote agent sessions and the browser and terminal Console. | [Pulse Agents](/pulse-agents/overview) | | [pulse-alerts](https://github.com/ignition-is-go/pulse-alerts) | Persisted alert state and notification delivery built on Myko. | [Browse source](https://github.com/ignition-is-go/pulse-alerts) | | [pulse-cluster](https://github.com/ignition-is-go/pulse-cluster) | Unreal fleet recipes, admitted operations, and PulseNode host actions. | [Pulse Cluster](/pulse-cluster/overview) | | [pulse-deploy](https://github.com/ignition-is-go/pulse-deploy) | Terraform provisioning and Ansible convergence, diagnostics, and maintenance. | [Pulse Deploy](/pulse-deploy/overview) | | [pulse-k8s](https://github.com/ignition-is-go/pulse-k8s) | GitOps manifests for the Pulse Agents Kubernetes deployment. | [Browse source](https://github.com/ignition-is-go/pulse-k8s) | | [pulse-lake](https://github.com/ignition-is-go/pulse-lake) | Parquet storage and DataFusion queries for fleet telemetry. | [Browse source](https://github.com/ignition-is-go/pulse-lake) | | [rship-control-plane](https://github.com/ignition-is-go/rship-control-plane) | Legacy Rocketship deployment inventories and Docker Swarm management. | [Browse source](https://github.com/ignition-is-go/rship-control-plane) | | [tachyon](https://github.com/ignition-is-go/tachyon) | Typed admitted Ansible runs, execution, output streaming, and finalization. | [Browse source](https://github.com/ignition-is-go/tachyon) | ## Development | Repository | Responsibility | Documentation | | --- | --- | --- | | [cargo-flux](https://github.com/ignition-is-go/cargo-flux) | Dependency-aware task planning across Cargo, JavaScript, and Python projects. | [Browse source](https://github.com/ignition-is-go/cargo-flux) | | [css-styled](https://github.com/ignition-is-go/css-styled) | Typed CSS generation for Rust component libraries. | [Browse source](https://github.com/ignition-is-go/css-styled) | | [gpui-command-palette](https://github.com/ignition-is-go/gpui-command-palette) | Command palette components for GPUI applications. | [Browse source](https://github.com/ignition-is-go/gpui-command-palette) | | [gpui-mullion](https://github.com/ignition-is-go/gpui-mullion) | Split panes and activity surfaces for GPUI applications. | [Browse source](https://github.com/ignition-is-go/gpui-mullion) | | [gpui-node-graph](https://github.com/ignition-is-go/gpui-node-graph) | Node-graph editor components for GPUI applications. | [Browse source](https://github.com/ignition-is-go/gpui-node-graph) | | [hyphae](https://github.com/ignition-is-go/hyphae) | Concurrent reactive dataflow used by application frameworks. | [Hyphae](/hyphae/hyphae/readme) | | [leptos-command-palette](https://github.com/ignition-is-go/leptos-command-palette) | Command palette components for Leptos applications. | [Browse source](https://github.com/ignition-is-go/leptos-command-palette) | | [leptos-node-graph](https://github.com/ignition-is-go/leptos-node-graph) | Node-graph editor components for Leptos applications. | [Browse source](https://github.com/ignition-is-go/leptos-node-graph) | | [leptos-resize](https://github.com/ignition-is-go/leptos-resize) | Shared drag-resize behavior for Leptos interfaces. | [Browse source](https://github.com/ignition-is-go/leptos-resize) | | [mullion](https://github.com/ignition-is-go/mullion) | Split panes and activity bars for Leptos applications. | [Browse source](https://github.com/ignition-is-go/mullion) | | [myko](https://github.com/ignition-is-go/myko) | Event-sourced application state, queries, and cross-language bindings. | [Myko](/myko/readme) | | [pulse-docs](https://github.com/ignition-is-go/pulse-docs) | This documentation site and its content build. | [Browse source](https://github.com/ignition-is-go/pulse-docs) | | [pulse-gpui](https://github.com/ignition-is-go/pulse-gpui) | Pulse design system and component library for GPUI. | [Browse source](https://github.com/ignition-is-go/pulse-gpui) | | [pulse-leptos-ui](https://github.com/ignition-is-go/pulse-leptos-ui) | Pulse design system and component library for Leptos. | [Browse source](https://github.com/ignition-is-go/pulse-leptos-ui) | | [pulse-website](https://github.com/ignition-is-go/pulse-website) | Public Pulse marketing website. | [Browse source](https://github.com/ignition-is-go/pulse-website) | | [pulse](https://github.com/ignition-is-go/pulse) | Cross-repository workspace CLI and signed product-command distribution. | [Pulse CLI](/pulse-cli/overview) | ## Work across repositories Use [Pulse CLI workspaces](/pulse-cli/workspaces) to materialize the repositories a task needs. Each repository has its own history, dependency versions, and release cycle. Read its checked-out instructions before changing code. --- slug: /pulse/concepts title: Glossary source: pulse --- # Glossary Shared terms used across Pulse. Product-specific APIs and deployment settings belong in the product documentation. ## Applications and control **Rocketship** is Pulse's reactive multimedia and show-control platform. It connects applications through shared entities, controls, and observations. Its repository name is `rship`. **Executor** is an application, plugin, or process that exposes capabilities to Rocketship. Executors register instances and targets, handle actions, and publish observations through emitters. See [Executor development](/rship/executor-development/overview). **Instance** is a registered occurrence of a service in Rocketship. A service can have multiple instances on different machines. **Target** is an addressable unit of control exposed by an executor. Targets group capabilities such as actions, emitters, and properties. **Action** is a command exposed by an executor for changing a target's behavior or state. **Emitter** publishes observations or events from a target. A **pulse** is an event sent through an emitter. **Property** represents a value associated with a target. Property behavior and ownership follow the SDK and server contracts. **Scene** contains Rocketship programming. A **session** provides the context in which that programming is used. See the [Rocketship data model](/rship/core-concepts/data-model) for their relationships. **SDK** is a library or host-application integration used to connect software to Rocketship. [Rocketship SDKs](/rocketship-sdks/overview) includes language SDKs, RshipExec for Unreal Engine, and Rship TouchDesigner. ## Assets and media **Publish** is a versioned asset output in [Pipe](/pulse-pipe/docs/24-user-guide). Its manifest records representations and provenance. **Representation** is one form of an asset, such as a mesh or texture output, recorded by the publishing pipeline. **Bundle** selects published asset versions for downstream use in Pipe. The word can have other product-specific meanings; use the owning product's definition. **Screen** is a logical canvas in Phosphor's rendering model. An **Output** is a completed render feed. Placements map Screen geometry into Outputs. See [Phosphor](/phosphor/readme). **Pixel Streaming** delivers Unreal-rendered video and interaction through WebRTC. It is separate from asset publishing and Unreal cluster lifecycle. **SMPTE ST 2110** is a family of standards for carrying separate professional media streams over IP networks. **NMOS** provides discovery, registration, and connection-management specifications used with networked media. **PTP**, the Precision Time Protocol, synchronizes clocks over a network. A deployment must verify clock health and the timing requirements of its media path. **Frame synchronization** coordinates when renderers produce or present frames. The mechanism depends on the renderer and transport; a shared clock alone does not prove synchronized output. **Rivermax** is NVIDIA's SDK for high-performance media transport, including SMPTE ST 2110 workflows. ## Infrastructure and fleet **Cluster** needs context. A Pulse Cluster recipe describes an Unreal render group. Rocketship can group application instances. A Kubernetes cluster manages container workloads. These are different objects. **Render pool** is the fleet of hosts available for Unreal workloads. Pulse Cluster selects launch-time roles and dispatches operations to those hosts. **Recipe** is durable Unreal launch intent managed by Pulse Cluster. Editing a recipe and deploying it are separate operations. **PulseNode** performs typed host actions for Pulse Cluster, including Unreal lifecycle and project materialization. **Convergence** applies infrastructure configuration to bring a host or service into the declared state. [Pulse Deploy](/pulse-deploy/automation) owns this work; Unreal lifecycle belongs to Pulse Cluster. **Previs** means previsualization. Its role and relationship to production output depend on the project and deployment. **nDisplay** is Unreal Engine's system for distributed rendering across multiple nodes and displays. **Concert** is Unreal's multi-user editing system. **Hypervisor** runs virtual machines. A **VM** is a guest operating system with virtualized hardware. A container shares a host kernel rather than providing a full guest machine. **NAS** is network-attached storage. **Plastic SCM** is the version-control system used by Pulse project-content workflows. **NetBird** provides private network connectivity and access control between hosts. Application authentication remains separate. ## Agents and development **Agent session** is an agent's execution and interaction state. [Pulse Agents](/pulse-agents/overview) keeps remote sessions and transcripts on its control plane; a Console pane is a view into a session. **Harness** is the runtime that hosts an agent and connects its model, tools, and interaction protocol. In repository references, `harness` can also mean Pulse's separate reliability and workflow library. **MCP**, the Model Context Protocol, connects agent clients to tools and resources. [Myko MCP](/myko-mcp/overview) exposes the operations registered by a connected application. **Claim** records who is working on a task in [Levi](/levi/readme). Completion follows Git ancestry, so a task can be complete on one branch and still open on another. **Workspace** is a set of isolated repository checkouts managed by [Pulse CLI](/pulse-cli/workspaces). Each repository keeps its own Git history. Pulse also uses `workspace` in product-specific contexts, such as a content working copy. **Myko** supplies event-sourced application state and reactive queries. **Hyphae** supplies concurrent reactive dataflow. Their [framework docs](/myko/readme) and [dataflow docs](/hyphae/hyphae/readme) live under Development. --- slug: /pulse/docs-for-agents title: Docs for agents source: pulse --- # Docs for agents Use this guide to find Pulse documentation, choose the right agent tool, and start work in a repository. ## Read the documentation 1. Fetch [`/llms.txt`](/llms.txt) from the docs site's origin. It lists published pages under the same categories and products as the sidebar. 2. Find the product relevant to the task. 3. Fetch the page's `.md` link. Preserve the source and path exactly as listed in the index. 4. Follow the page's upstream repository link when you need current code or instructions for that repository. For example, with `PULSE_DOCS_URL` set to your docs site's origin: ```bash curl -fsS "$PULSE_DOCS_URL/llms.txt" curl -fsS "$PULSE_DOCS_URL/pulse-pipe/docs/24-user-guide.md" curl -fsS "$PULSE_DOCS_URL/phosphor/readme.md" ``` The HTML routes render through a client-side application. A plain HTTP fetch of an HTML route does not contain the page body. Use the static Markdown endpoints for reading without a browser. ### Find the right scope Read the [platform overview](/pulse/index.md) for product responsibilities. Use the [repository directory](/pulse/repository-directory.md) to locate source code, including libraries without a standalone product page. The navigation categories describe purpose: Production, Operations, and Development. [Pulse CLI](/pulse-cli/overview.md) appears separately under Developer tools. These categories are different from the architectural pillars of infrastructure, orchestration, and capabilities. ### Available formats | Endpoint | Content | | --- | --- | | [`/llms.txt`](/llms.txt) | Page index with Markdown links and short excerpts, grouped by category and product. | | `//.md` | One page's original Markdown with added `slug`, `title`, and `source` frontmatter. | | [`/llms-full.txt`](/llms-full.txt) | All published pages concatenated, with each page's slug, title, and source. Use when the task needs the full corpus. | | [`/robots.txt`](/robots.txt) | The site's crawler and AI content signals. | Append `.md` to the page path before a fragment or query string. For example, `/rship/core-concepts/terms#executor` becomes `/rship/core-concepts/terms.md#executor`. Links inside the Markdown retain their source form. Some point to HTML routes or upstream files that are not imported here. Use `/llms.txt` to confirm published Markdown paths. Resolve root-relative links against the docs site's origin. `Accept: text/markdown` does not select a Markdown response on this site. Request the explicit `.md` path. In a browser, the **Agents toolkit** provides **Copy as Markdown**, **Open MD in new tab**, and links that open Claude or ChatGPT with the page URL. Those links provide context to a separate conversation; they do not connect the model to Pulse services. ## Choose the right tool Reading this site does not establish a connection to a running Pulse service. Discover the tools and connection state available in your current agent session. | Need | Tool or product | Responsibility | | --- | --- | --- | | Coordinate with live agent sessions | [Marshal](/marshal/readme.md) | Presence and messaging across agents, harnesses, and machines. | | Track work and dependencies | [Levi](/levi/readme.md) | Tasks, claims, and completion tied to Git ancestry. | | Create an isolated checkout | [Pulse CLI](/pulse-cli/overview.md) | Materialize selected repositories through `pulse work`. | | Discover and call application operations | [Myko MCP](/myko-mcp/overview.md) | Access the operations registered by the connected Myko application. | | Run durable remote agent sessions | [Pulse Agents](/pulse-agents/overview.md) | Session execution, transcripts, approvals, and the Console. | | Operate the Unreal render fleet | [Pulse Cluster](https://github.com/ignition-is-go/pulse-cluster) | Durable recipes, admitted lifecycle operations, and PulseNode actions. | | Provision or converge infrastructure | [Pulse Deploy](/pulse-deploy/automation.md) | Terraform and Ansible configuration, diagnostics, repair, and maintenance. | ### Discover Myko operations before calling them Myko MCP exposes `connection_status`, `search`, and `execute`. The operations behind those tools depend on the connected application and its filters. 1. Inspect `connection_status` to confirm the connection. 2. Use `search` to find the relevant query, view, report, or command. 3. Request detailed search results to get wire argument names, result wrappers, and validated examples when available. 4. Use `execute` with the discovered operation contract. Return the fields needed for the task. Do not assume that every Myko application registers the same operations. A successful documentation fetch does not prove that an application is reachable or that an operation is available. The in-server MCP endpoint is `/myko/mcp` on the application's Myko gateway. Obtain its host, port, and connection configuration from that application's deployment. See [Myko's MCP reference](/myko/readme.md) for transports and client configuration. ### Use the current fleet control plane Pulse Cluster owns Unreal cluster intent and lifecycle. PulseNode performs typed host actions and project materialization. Ansible owns infrastructure convergence, diagnostics, repair, and bounded maintenance. Use the deployed `pulse cluster --help` and the [Pulse Cluster source documentation](https://github.com/ignition-is-go/pulse-cluster) for the current command surface. See [Pulse Deploy automation](/pulse-deploy/automation.md) for the boundary between fleet lifecycle and infrastructure convergence. ## Start repository work 1. Read the workspace's instructions and inspect the task with `levi next --json` when choosing work. 2. Create a scoped checkout with `pulse work create --task `. For work without a task, use `--name `. 3. Enter the printed path. Read the target repository's `AGENTS.md`, or its legacy `CLAUDE.md`. 4. Commit changes inside the repository that owns them. Use `pulse work add ` if the task needs another repository. [Pulse CLI workspaces](/pulse-cli/workspaces.md) covers checkout creation, local dependency overrides, and retirement. The `pulse` tooling repository follows the same isolated-checkout rule as the managed repositories. For foundational repositories such as `myko`, `hyphae`, or `harness`, inspect Marshal's live roster as required by the workspace instructions. Follow those instructions for task completion and workspace retirement. ## Check source freshness This site aggregates selected files. Some pages are checked-in snapshots; others are fetched from a configured upstream ref when the docs site builds. It is not a live mirror of every repository or deployment. For implementation work, verify the checked-out source, dependency versions, and deployed command surface. Repository instructions apply to that checkout. Imported prose and examples can describe an older version. The site's `robots.txt` currently declares `ai-train=yes`, `search=yes`, and `ai-input=yes`. These content signals describe documentation usage. They do not grant repository access or permission to operate infrastructure. --- slug: /pulse-cli/overview title: Overview source: pulse-cli --- # Pulse CLI The `pulse` command manages Pulse development workspaces and installs operator CLIs. It gives developers and agents one entry point for repository state, isolated checkouts, and product commands. The CLI lives in the [pulse repository](https://github.com/ignition-is-go/pulse). That repository is the cross-repository workspace for Pulse; each managed project keeps its own Git history and release cycle. ## Install and update Use the installer from the private Pulse distribution service. Obtain its base URL from your environment's access-controlled setup instructions and set `PULSE_DISTRIBUTION_URL` to that URL. Access to the service is required. Linux and macOS: ```sh curl -fsSL "$PULSE_DISTRIBUTION_URL/pulse/install.sh" | sh ``` Windows PowerShell: ```powershell irm "$env:PULSE_DISTRIBUTION_URL/pulse/install.ps1" | iex ``` For an existing installation: ```sh pulse update ``` ## Inspect the workspace Run these commands from the main Pulse checkout or a scoped workspace: ```sh pulse repos pulse status pulse sync ``` `status` reports repository state without changing files. `sync` clones missing repositories or fast-forwards eligible branches. It does not switch an existing checkout's branch or overwrite local changes. A scoped workspace manages only its materialized repositories. Naming a repository explicitly selects it, so `pulse sync marshal` can materialize Marshal even when it is not already present. For structured command output, place `--json` before the command: ```sh pulse --json status ``` ## Install product commands Operator CLIs are signed components managed by Pulse: ```sh pulse components list pulse components install cluster pulse components update pulse components rollback cluster ``` Installed components run through the Pulse command: ```sh pulse cluster --help pulse agents --help pulse optik --help pulse tactus --help ``` Use `pulse components list` to inspect available versions. Installing a CLI does not establish access to a product's server. ## Create an isolated checkout Use [Workspaces](workspaces.md) to create a checkout for the repositories you will change. For agent tools and task claims, see [Docs for agents](/pulse/docs-for-agents). `pulse --help` and each command's `--help` describe the installed release. The [source README](https://github.com/ignition-is-go/pulse#the-pulse-cli) documents installation and workspace behavior. --- slug: /pulse-cli/workspaces title: Workspaces source: pulse-cli --- # Workspaces A Pulse workspace contains isolated Git worktrees for selected repositories. Each repository retains its own commits, branches, and releases. Build caches are shared within a workspace and isolated between workspaces. ## Create a workspace From the main Pulse checkout, choose the repositories required by the task: ```sh pulse work create myko hyphae --name reactive-change ``` For a Levi task, inspect available work without claiming it, then pass the task ID to Pulse: ```sh levi next --json pulse work create myko --task ``` Enter the workspace path printed by the command. Read each repository's `AGENTS.md`, or its legacy `CLAUDE.md`, before editing. Commit each change inside the repository that owns it. The `pulse` tooling repository follows the same rule: ```sh pulse work create pulse --name cli-change ``` ## Change the repository selection Inside the scoped workspace: ```sh pulse work add marshal pulse work remove marshal ``` Removal refuses a dirty checkout or commits that are not reachable from a fetched remote ref. ## Use local dependencies Generate workspace-local Cargo overrides for materialized repositories: ```sh pulse work patch ``` Adding or removing repositories refreshes an active override block. Remove the generated overrides with: ```sh pulse work unpatch ``` ## Inspect and retire workspaces Inspect managed workspaces before removing one: ```sh pulse work list pulse work list ``` After the work has landed and the checkout is clean: ```sh pulse work retire --confirm ``` If you are abandoning a branch, `--abandon` permits retirement once its commits are preserved on a remote. It does not require the branch to have merged. Pulse refuses retirement when its lifecycle checks fail. Resolve the reported state before retrying. See the [workspace source documentation](https://github.com/ignition-is-go/pulse/blob/main/docs/workspace-isolation.md) for the isolation model. --- slug: /rship/get-started/introduction title: Introduction source: rship --- # Introduction Rship is the control orchestration layer of [Pulse](/pulse/index). External services — UE, TouchDesigner, lighting, cameras, audio — connect through executors. Rship routes events between them and lets you program declarative bindings instead of hard-coded integrations. ## Architecture Hub-and-spoke. The server is the hub; clients (web UI) and executors (plugins or sidecars) are the spokes. ``` ┌─────────────────────────────────────────────────────────────────┐ │ RSHIP SERVER │ │ Routes control data between clients and executors │ └─────────────────────────────────────────────────────────────────┘ ▲ ▲ │ commands / state │ commands / state ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Rship Clients │ │ Executors │ │ (web UI, etc.) │ │ (plugin/sidecar) │ └──────────────────┘ └──────────────────┘ ▲ │ native API ▼ ┌──────────────────┐ │ Services │ │ (UE, TD, etc.) │ └──────────────────┘ ``` - **Rship Server** — routes control data between clients and executors. - **Rship Client** — web UI for orchestrating bindings and scenes. - **Executor** — plugin or sidecar that bridges an external service. - **Service** — the external system the executor wraps (UE, TD, etc.). For a hands-on starting point, walk through [Connect an executor](../executor-development/connect-executors). --- slug: /rship/core-concepts/terms title: Terms source: rship --- # Terms ## Core - **Rship Servers**: The core servers which run rship and manage data flow between all connected Executors. - **Rship Clients**: Clients that connect to rship servers (Rship UI and Executors). - **Rship UI**: The UI for programming rship. ## External > Represents all systems involved in the installation and defined by external Executors. **Machines** run **Executors** which create **Rship Clients** that connect to **Rship Servers** and provide **Schemas** for **Instances** of **Services**, as well as **Targets**, **Emitters**, and **Actions**. - **Machines**: Physical or virtual devices. - **Executors**: Plugins or sidecar apps that connect to rship, define Schemas, and enable bidirectional communication. - **Schemas**: Descriptions of what Executors provide and validations for what they can receive. - **Services**: Software running on Machines. - **Targets**: Interactable entities; the logical units of control in rship. - **Actions**: Commands for changing the state of Targets. - **Payloads**: Data carried with an Action. - **Emitters**: Observers and reporters of state changes for Targets. - **Pulses**: Data sent by an Emitter. - **Instances**: Copies of a Service running across Machines. Instances tie particular Services to particular Executors. - **Clusters**: Groups of Instances of the same Service. > All of the above is available in all projects on the same server. ## Internal > Represents internal rship entities and concepts. Everything below is scoped by project. Users connect to the **Rship UI** in a browser to manage **Sessions**, build **Scenes**, and do other rship programming. - **Projects**: Top-level organizational containers that encapsulate and scope internal rship programming. - **Overview**: A summary view of the project and its components. - **Sessions**: Configurations of Machines, Services, Instances, and Clusters. Sessions determine which Instances are used as live data for Services. Projects can have multiple Sessions. - **Scenes**: Encapsulations of sets of Bindings and other rship programming; the logical unit of design in rship. - **Scene Activation Status**: A Scene's Active / Inactive / Armed / Disarmed state. - **Active**: All effects the Scene has on the rest of the system are enabled. Actions and other control nodes that change the state of something else do send. - **Inactive**: All effects the Scene has on the rest of the system are disabled. Actions and other control nodes that change the state of something else don't send. - **Armed**: The Scene can be Activated. - **Disarmed**: The Scene cannot be Activated. - **Nodes**: Units of programming that live on Scene Graphs. Emitters and Actions are represented by Nodes, as well as other utility functions. - **Connections**: Links between Nodes in Scene Graphs. - **Variables**: Project-global named values that can be get/set in Scenes. - **Event Tracks**: Data over time. Can be manually programmed or sourced by Executors. Used to Build On/Build Off Scenes or fired within Scenes. Data produced by Event Track Playbacks can be used in the same way as Emitter data. - **Playbacks**: Running instances of Event Tracks. - **Lanes**: Channels of data within Event Tracks that organize Keyframe data. - **Keyframes**: Recorded values in time with optional fade durations and Curves. Keyframes can reference Scene Outputs and Global Variables for their values. - **Scene Outputs**: Data produced by Scenes that can be consumed by other Scenes or in Keyframe values. - **Curves**: Easing functions used in transitions over the fade durations of Keyframes. - **Bindings**: Reactive event relationships between Emitters and Actions with optional intermediary programming. - **Assets**: Abstract representations of static data, independent from the data itself. Services are to Instances as Assets are to Files. Users program against Asset names and version constraints, and Files themselves are resolved as needed. - **Files**: Items discovered by scanning a machine's storage. - **Scheduling**: Planning when and how Scenes are activated and deactivated in time or with conditional logic. - **Calendars**: Global timelines where Scene Activation States are scheduled in world clock time. - **Appearances**: Scenes scheduled on Calendars with start and end times. - **Playlists**: Collections of Appearances. - **Constraints** (roadmap): Rules for when and how Scenes can be Appearances in a Calendar. Constraint-based scheduling supports contractual restrictions and dependencies for Scene Appearances. - **Tags** (roadmap): User metadata for organizational purposes. --- slug: /rship/core-concepts/data-model title: Data Model source: rship --- # Data Model ![Core diagram showing rship entity relationships](./core-diagram-01.jpg) --- slug: /rship/core-concepts/visualizer title: Visualizer source: rship --- # Visualizer The Visualizer is the spatial control surface for Pulse projects. It combines a live Three.js viewport with management tools for lighting rigs, targets, points of interest, and camera calibration. Use it to validate imports, stage looks, and keep the scene graph, automation targets, and hardware placements in sync. ## Workspace layout - **Sidebar** – Lists imported lighting bundles, active session targets, saved points of interest, and registered cameras. Selecting any item highlights it in the viewport and syncs selection with the scene graph. - **Viewport** – A real-time Three.js scene driven by Pulse data. The camera toolbar supports preset changes, resets, and focus commands driven by POIs or targets. - **Inspector** – Surfaces selection details, session metadata, and live status callouts so operators can keep context while editing. ## Importing lighting data (MVR + GDTF) - Use **Import MVR** to bring in rig geometry, fixture placement, and pre-patched universes. Each import is stored as a bundle that can be edited, hidden, or re-synced. - Use **Add GDTF** to register new fixture personalities before they appear in an MVR. Fixtures without a matching GDTF profile are flagged in the status rail with guidance on resolving the mismatch. - Imported bundles are versioned alongside the project’s asset library so they can be promoted through staging environments without re-uploading. ## Targets, bundles, and automation - Selecting a bundle issues a `SetSelectedBundles` command so scene editors, event tracks, and the visualizer stay in lockstep. - Target selection mirrors the active session, enabling quick “solo” workflows for verifying effects, calibration offsets, or automation cues. - Use the Inspector to review payload counts, service ownership, and the scope of each bundle before triggering automations downstream. ## Points of interest - Points of interest (POIs) are project-scoped XYZ coordinates. They unlock consistent camera moves, stage blocking, and calibration anchors. - POIs can be created in the Points tab, calibration flows, or automation scripts. Once saved, they appear in the sidebar with tools to frame the camera instantly. - Focus commands send a `cameraCommands` `FIND` instruction so the viewport camera transitions smoothly without breaking an operator’s workflow. ## Camera and calibration alignment - The Cameras panel lists registered sources, router inputs, and preferred still-resolution pairings. - Use camera resets to snap back to production framing when experimentation moves too far off axis. - Calibration-specific diagnostics (live drift, solver status, reference snapshots) surface in the Inspector as the calibration suite develops. Keep a look out for new badges in the live data feed with each release. ## Next steps - Read the [Data model](data-model.md) for graph entities and their relationships. - Review [Terms](terms.md) for targets, bindings, and executor terminology. - Follow [Rocketship releases](https://github.com/ignition-is-go/rship/releases) for published changes. --- slug: /rship/executor-development/overview title: Overview source: rship --- # Overview ## How does rship work? Rship uses abstractions to generalize and unify how different subsystems communicate about control data. The core entities and their relationships are illustrated below: ![Core diagram showing rship entity relationships](./core-diagram-02.jpg) Rship defines a system-agnostic language, and subsystems use this language to provide their own unique schema for the core entities: ```json // Target: An interactable entity { "properties": { "id": { "type": "string" } } } ``` ```json // Action: A command that can be invoked { "properties": { "id": { "type": "string" }, "targetId": { "type": "string" } } } ``` ```json // Payload: Data sent with an Action { "properties": { "id": { "type": "string" }, "targetId": { "type": "string" }, "actionId": { "type": "string" }, "data": { "type": "number" } } } ``` ```json // Emitter: A source of data { "properties": { "id": { "type": "string" }, "targetId": { "type": "string" } } } ``` ```json // Pulse: Data produced by an Emitter { "properties": { "id": { "type": "string" }, "emitterId": { "type": "string" }, "data": { "type": "number" } } } ``` ```json // Service: A software system that provides Targets { "properties": { "id": { "type": "string" }, "type": { "type": "string" } } } ``` ```json // Instance: A running copy of a Service { "properties": { "id": { "type": "string" }, "serviceId": { "type": "string" }, "machineId": { "type": "string" } } } ``` ```json // Machine: A physical or virtual computer { "properties": { "id": { "type": "string" }, "address": { "type": "string" } } } ``` This approach allows rship to be a universal translator for control data, enabling arbitrary groups of connected subsystems to communicate with each other. We also avoid needing to build custom integrations between every pair of subsystems, which would become increasingly problematic as the number of subsystems grows. Every system doesn't need to be able to talk to every other system — they just need to be able to talk to rship, and so the complexity of creating a unified control context grows linearly rather than quadratically. We do still need to build integrations, but only once for each subsystem, in the form of an **Executor**. Executors facilitate the connection between a subsystem and rship. The rship SDK makes it straightforward to rapidly build executors for many kinds of subsystems, running either as an embedded plugin or sidecar application. Executors integrate deeply, maintaining parity between the internal state of a subsystem and how it is represented in rship. Because executors dynamically generate schema, changes to the subsystem's control schema automatically propagate through to the control layer. --- slug: /rship/executor-development/connect-executors title: Connect an Executor source: rship --- # Connect an Executor ## Build a basic reactive system with TouchDesigner In this guide, we'll use [rship-touchdesigner](https://github.com/ignition-is-go/rship-touchdesigner) to demonstrate how to connect an executor to rship and expose control parameters. ### Prerequisites - ZeroTier installed and connected to rship VPN - `rship_[version].tox` downloaded from the [rship releases page](https://github.com/ignition-is-go/rship/releases) ### Core Concepts - **Emitters** are data sources. - **Actions** are data providers. - **Bindings** create reactive event relationships between **emitters** and **actions**. - **Instances** are running copies of a **service**. - **Services** run on **machines**. - **Machines** are physical or virtual computers. ### Open rship Open the Rocketship UI at the URL provided for your environment. > If you can reach the UI, that means you can connect to rship from your machine. Create a new project, and go to the sessions tab. ### Configure TouchDesigner Open two copies of TouchDesigner. Name one `Square.toe` and the other `Wave.toe`. Add the `rship_[version].tox` to both networks. ![rship-touchdesigner rship.tox](./connect-executors/rship-touchdesigner-01.png) In the `rship` base, set the address field to a valid rship server address provided for your environment. Save the .toe or hit 'reconnect' on the `rship` base, and you should see the **instances** appear in rship right away. ![deactivated instances](./connect-executors/instances-01.png) Activate the **instances** to make them available in the session. ![activated instances](./connect-executors/instances-02.png) ## Adding Targets Back in TouchDesigner, we need to make some Base COMPs to expose **targets** in rship. Create a `control` base in each network, tag those bases 'rship', and save the .toe. ### Core Concepts - **Target**: an interactable entity ![rship-touchdesigner Base COMP](./connect-executors/rship-touchdesigner-02.png) Navigate to the scenes tab in the rship UI. You should see **targets** appear in the **target** list right away. ![targets list](./connect-executors/targets-01.png) With rship-touchdesigner, every Base COMP is a top-level **target** with **actions** to enable/disable (bypass) the COMP. Click and drag **actions** into the scene graph to create nodes. ![scene graph](./connect-executors/scenes-01.png) **Actions** can be fired manually by clicking 'go' on the node. Try disabling and enabling the COMPs from rship. > Make sure the scene is active! ### Expose parameters In Square.toe, create a Rectangle TOP. Right click on the 'control' base and select 'Customize Component...', drag the 'Fill Color' parameter onto the Component Editor window, then save the .toe. ![component editor](./connect-executors/rship-touchdesigner-03.png) In TouchDesigner, parameters can be exported in three ways: - Bind as Master (purple) - Bind as Reference (purple) - Set Reference Expression on Source (blue) TouchDesigner bindings are read-write but references are read-only, so rship won't create **actions** for pars exported as references (forthcoming!); however, TouchDesigner references are more performant than TouchDesigner bindings. In Wave.toe, create an LFO CHOP. To expose the value of the lfo, open the Component Editor window as well as the Parameters window of the 'control' base. Manually create a float parameter, toggle the 'lfo' viewer active (small plus button in the corner), and drag the channel onto the parameter. Select 'Export CHOP' or 'CHOP Reference', then save the .toe. ![LFO export](./connect-executors/rship-touchdesigner-04.png) Now in rship you should see the exposed parameters as sub**targets**. > If **targets** don't show up on .toe save, hit 'reconnect' on the `rship` base. ![sub-targets](./connect-executors/targets-02.png) ### Make Bindings Drag an LFO Value Updated **emitter** and Set Fill Color **action** onto the scene graph. Make three **bindings**, one from the **emitter** outlet to each **action** inlet. ![bindings](./connect-executors/scenes-02.png) You should now see the square animating in Square.toe. ### Core Concepts - **Emitters** are data sources. - **Actions** are data providers. - **Bindings** create reactive event relationships between **emitters** and **actions**. - **Instances** are running copies of a **service**. - **Services** run on **machines**. - **Machines** are physical or virtual computers. ## Next Steps - **[Executor Development Overview](./overview)** — Understand the rship data model and how executors fit into it. --- slug: /pulse-unreal/readme title: pulse-unreal source: pulse-unreal --- # pulse-unreal Pulse ecosystem utility plugins and Unreal Engine integration for [Rocketship](https://docs.rship.io) — a reactive control platform for live entertainment, broadcast, and immersive experiences. **Requires** Unreal Engine 5.7+ on Windows or macOS, plus a Rocketship server for full functionality. ## Plugins | Plugin | Description | Status | | ------------------------------------------------------------ | ------------------------------------------------------------------------- | ------------ | | [RshipExec](Plugins/RshipExec/README.md) | Core executor — WebSocket, targets, emitters, actions, fixtures, bindings | Stable | | [RshipField](Plugins/RshipField/README.md) | GPU field simulation, sampling, mesh deformation, rotunda control | Beta | | [RshipRotundaMullion](Plugins/RshipRotundaMullion/README.md) | Optimus deformer for rotunda mullion/atom mesh, layered GPU control | Beta | | [RTLightColor](Plugins/RTLightColor/README.md) | Sample render target pixels and apply as colors to a grid of lights | Stable | | [ConcertHostIdentity](Plugins/ConcertHostIdentity/README.md) | Multi-User Editing display name + avatar color from machine hostname | Stable | | [Otel](Plugins/Otel/README.md) | Prometheus scrape and OTLP/HTTP push for engine and rendering metrics | Experimental | | [RivermaxTimecodeFix](Plugins/RivermaxTimecodeFix/README.md) | UE 5.7 RivermaxTimecodeProvider startup-race workaround | Stable | ## Principles These plugins are built around a few core tenets: **Define everything in code.** Plugin functionality lives in C++ — RHI commands, global shaders, programmatic render targets. Editor-authored `.uasset` is reserved for genuinely project-specific content (your consumer materials, your meshes, your scene composition). Every "the user authors X in editor" step before the plugin works is friction we try to eliminate. **Auto-discover, don't configure.** Controllers introspect components at runtime to expose parameters as rship actions and emitters — drop a Light Controller onto an actor with a LightComponent and it works, no parameter lists to maintain or schema files to author. The same principle drives Blueprint integration: the `RS_` prefix is the entire contract for exposing logic. No custom node types, no metadata specifiers, no separate registration step — name a variable, function, or delegate `RS_Foo` and rship sees it. **Composable, single-purpose controllers.** Each controller exposes one subsystem — lights, materials, cameras, transforms, Niagara. The same actor can carry several, and each builds its own sub-target. There's no monolithic "RshipActor" — you opt in to exactly what you need, and you can ship your own controllers alongside the built-in ones without forking the plugin. **Built for live production.** Live shows, rehearsals, previs, and installation work all share the same demand: the plugin can't be the thing that breaks the workflow. Dropped connections reconnect and fully re-register their target tree. Components added or removed mid-session refresh their parent registration automatically. Registration itself is batched and deduplicated so re-syncs stay incremental. The system is designed to absorb the chaos of live production rather than enforce a particular order of operations. ## Example [**RshipExec**](Plugins/RshipExec/README.md) is the main plugin. It connects your UE project to the rship server and exposes anything you mark with the `RS_` prefix: ```cpp UPROPERTY(EditAnywhere, BlueprintReadWrite) float RS_Intensity = 1.0f; UFUNCTION(BlueprintCallable) void RS_PlayCue(FName Cue); ``` `RS_Intensity` becomes both an **action** (rship can write to it) and an **emitter** (UE pushes changes back). `RS_PlayCue` becomes an action with a `Cue` parameter. Built-in controller components — Light, Material, Camera, Transform, Niagara, Substance, and more — auto-discover parameters from their target components, so most fixtures need zero code. See [**RshipExec/README.md**](Plugins/RshipExec/README.md) for the full guide: connection, controllers, Blueprint integration, and writing your own controllers. ## Installation 1. Copy the `Plugins/` folder (or individual plugins you need) into your UE project's `Plugins/` directory. 2. Enable each plugin in your `.uproject`: ```json { "Plugins": [{ "Name": "RshipExec", "Enabled": true }] } ``` 3. Open the relevant plugin's README for plugin-specific setup. ### Bumping (consumers using a pulse-unreal clone) Sites that consume this repo as a vendored clone (e.g. cloned into a Plastic workspace at `ue/pulse-unreal/` and referenced via `AdditionalPluginDirectories` in the project's `.uproject`) can drive the update cycle with `scripts/Update-PulseUnreal.ps1`: it fetches, fast-forward pulls when behind (and reports a divergence instead of failing), then **always** rebuilds `RshipSourceEditor` — so the binaries match `HEAD` even when git is already current — and prints a suggested commit subject. The operator runs the actual `cm ci` (or equivalent) to record the new state downstream. ### Launching previs locally `scripts/Start-Previs.ps1` launches the previs project under the same render policy the deployment IaC applies to a render-pool previs receiver — GC, DLSS, screen percentage, Remote Control and display-mode flags all composed the same way. It is self-contained: no controller, no Ansible, no cluster definition. Per-cluster values that a rendered definition would supply are parameters carrying the deployed defaults. `-DryRun` prints the command line without launching. Genlock, PTP and the 2110 receive path are opt-in (`-Genlock`, `-ReceiveConfig`): the deployment emits those unconditionally because every render node clocks off the venue grandmaster, and without one a Rivermax custom time step never synchronizes and stalls the game thread. `-AllowClose` relaxes the deployment's `bAllowClose=false`, which otherwise produces a window that cannot be closed. `NodeSettings` cannot be delivered on the command line at all. The project reads that ini through the `IniConfig` plugin, which resolves it by absolute path and loads it with `FFileHelper`, so the values never pass through `GConfig` and UE's `-ini:NodeSettings:[Section]:Key=` override silently does nothing. The launcher therefore leaves the file alone by default. `-NodeSettingsWrite saved` writes the plugin's override copy under `Saved/`, which is read ahead of the `Config/` defaults and is not version-controlled; `-NodeSettingsWrite config` targets the tracked file, with a `.bak` mirror. Because that composition is a second implementation of the deployment's, `scripts/Assert-PrevisLaunchParity.ps1` diffs the two flag sets and exits non-zero on divergence in either direction. Run it after changing either side. ## Support - GitHub Issues: [github.com/ignition-is-go/pulse-unreal](https://github.com/ignition-is-go/pulse-unreal/issues) - Rocketship Docs: [docs.rship.io](https://docs.rship.io) ## License Proprietary — contact [Lucid](https://lucid.rocks) for licensing. --- slug: /pulse-unreal/plugins/rshipexec/readme title: RshipExec source: pulse-unreal --- # RshipExec Plugin Changes affecting connection lifecycle, topology, scene state, data layers, presence, streaming, or editor/cluster launch modes must follow the [RshipExec acceptance matrix](SPECS/ACCEPTANCE_MATRIX.md). Core Rocketship executor for Unreal Engine — connects your UE project to the rship control platform. > **Part of [pulse-unreal](../../README.md)** — see the main README for an overview of all plugins and shared design principles. ## Core Concepts ``` ┌─────────────────────────────────────────────────────────────────┐ │ RSHIP SERVER │ │ Bindings • Scenes • Calendars │ │ When Emitter changes → Trigger Action │ └─────────────────────────────────────────────────────────────────┘ │ Actions (commands) ▲ Pulses (state) ▼ │ ┌─────────────────────────────────────────────────────────────────┐ │ UNREAL ENGINE │ │ Target (Actor) ── Emitter ── Action ── … │ └─────────────────────────────────────────────────────────────────┘ ``` | Term | Definition | | ----------- | ----------------------------------------------------------------------- | | **Target** | An entity in rship (usually an Actor in UE) | | **Emitter** | A readable value that sends **Pulses** to rship (e.g., actor position) | | **Action** | A callable function that rship can invoke (e.g., SetColor) | | **Pulse** | A real-time state update from an Emitter | | **Binding** | A rule: "when this Emitter changes, trigger this Action" | ## Runtime Lifecycle and State Reconciliation RshipExec separates receiving programming from updating Unreal objects. In this documentation, a **UObject update** means resolving a registered target and writing an already-received rship value to its Unreal property or action. It does not mean launching Unreal or establishing the rship connection. ```text rship message received | v validate and acknowledge transport | v coalesce the latest desired value per target/action | v retain it while the world or target is unavailable | v target registers and streaming becomes ready | v update the UObject | v read back engine state and confirm convergence ``` ### Startup and map transitions - Interactive editor sessions connect immediately. They are not gated on BeginPlay or nDisplay readiness. - An initial nDisplay connection waits for engine facts that show the base world can safely receive programming: world and actor initialization, nDisplay scene startup, async and level streaming, visibility requests, and World Partition streaming. - After the first connection, map and data-layer transitions keep the transport alive. Only UObject updates are held; incoming desired state continues to be received and coalesced. - Data-layer requests are allowed to initiate streaming. The remaining scene programming is released after the requested data-layer states read back correctly and World Partition reports streaming complete. ### Desired-state convergence - RshipExec retains the newest desired value for a target that has not registered yet. When the actor/component appears, that value is applied; obsolete intermediate values are not replayed. - Target registration marks topology dirty. Registrations accumulated during streaming are published as one replacement snapshot instead of treating a partial target surface as authoritative. - Topology synchronization follows an ordered transaction: publish the current target surface, validate it with rship, replay desired state, then confirm the resulting Unreal state through readback. - Reconnects preserve desired state and clear only connection-specific transient work. Cold startup, reconnect, streamed-target registration, and divergent comp-graph recovery use the same convergence path. - Message admission, reconciliation, and UObject updates are bounded per frame so a startup burst cannot monopolize the game thread. - Receiving a command is not reported as a successful UObject update. Failed updates remain visible and retryable until a later update and readback succeeds. ### Runtime status `URshipRuntimeStatusLibrary::GetRuntimeStatus()` exposes raw lifecycle facts for external observers. These include world/map/streaming state, connection state, whether UObject updates are held, reconciliation progress, pending and retained desired state, retries and unresolved failures, topology validation, and retained-state convergence. Consumers should compose their own readiness policy from these facts rather than using log messages, timers, or inferred process state. ### HTTP listener control over WebSocket `URshipRuntimeControlLibrary::SetEngineHttpListenersEnabled(bool bEnabled)` controls all engine HTTPServer listeners while retaining Remote Control's routes for WebSocket calls. Call the function on `/Script/RshipExec.Default__RshipRuntimeControlLibrary` through Remote Control WebSocket. An HTTP call cannot stop its own active request. The result is `Applied`, `Busy`, or `Unavailable`. `Busy` means an HTTP request is still reading, processing, or writing; retry after it finishes. `Applied` means the requested operation was issued. Verify HTTP reachability and another WebSocket call independently. Passing `true` restarts listeners using their existing routes. This affects every consumer of the shared engine HTTPServer module. Start Remote Control's HTTP and WebSocket servers before the transition. Do not use `WebControl.StopServer` for this purpose: it also removes the routes used by generic WebSocket calls. Other modules can restart the listeners later; this operation does not enforce a persistent policy. Restricted Remote Control configurations must explicitly allow `RshipRuntimeControlLibrary.SetEngineHttpListenersEnabled`. ## Quick Start ### 1. Enable the plugin ```json // YourProject.uproject { "Plugins": [{ "Name": "RshipExec", "Enabled": true }] } ``` ### 2. Open the Rocketship panel Open it from **Window > Rocketship**. You'll also find specialized panels for fixtures, materials, and more. ![Window menu showing Rocketship panels](../../docs/screenshots/window-menu-rocketship-panels.png) ### 3. Connect to the server Enter your rship server address and port, then click **Reconnect**. The status indicator turns green when connected. ![Rocketship connection panel](../../docs/screenshots/connection-panel.png) Defaults live in **Project Settings > Game > Rocketship Settings** — see [Configuration](#configuration) below. ### 4. Register your actor Create or open a Blueprint Actor. In the Components panel, click **Add** and search for "rship". Add an **Rship Actor Registration** component — this declares the actor as the root rship target. Set its **Target Id** so rship has a stable name. ![Adding Rship components](../../docs/screenshots/add-rship-components.png) ### 5. Add controller components Controllers do the actual work of exposing things on the actor as rship actions and emitters. Each controller creates a child sub-target under the registration and auto-discovers parameters from the component or actor it inspects — no code required. Drop in whichever controllers you need; the same actor can carry several. See [Controllers](#controllers) for the full list. ### 6. (Optional) Expose Blueprint logic with `RS_*` For custom Blueprint behavior, add an **Rship BP Controller** component. It scans the owner actor (and, by default, sibling components) for properties and functions whose names start with `RS_`. See [RS_ Convention](#rs_-convention) for details. ![Creating an RS_ function](../../docs/screenshots/create-rs-function.png) ![Function with input parameters](../../docs/screenshots/function-input-parameters.png) ![Action implementation in Blueprint](../../docs/screenshots/function-implementation.png) ### 7. (Optional) React to variable changes If you need custom logic when an `RS_` variable is set from rship, bind the **On Rship Data** event on the Rship Actor Registration component: ![RshipTarget component events](../../docs/screenshots/rship-target-events.png) ![On Rship Data event handler](../../docs/screenshots/on-rship-data-event.png) ### 8. Press Play Hit Play in the editor — targets register with rship and appear in the UI. Create bindings between emitters and actions to build reactive behavior. ## Controllers Each controller exposes one subsystem on the owning actor. Drop the ones you need; each builds its own sub-target. | Controller | What it exposes | | ---------------------------------------- | ------------------------------------------------------------------------------ | | **Rship Light Controller** | LightComponent — intensity, color, temperature, plus type-specific params | | **Rship Material Controller** | Mesh material parameters, named variants, texture-set bulk swaps | | **Rship Standalone Material Controller** | Parameters on a directly-assigned MaterialInstanceDynamic | | **Rship Camera Controller** | Camera/CineCamera component properties | | **Rship Transform Controller** | Actor / scene-component transform | | **Rship Niagara Controller** | Niagara system user parameters | | **Rship Rig Controller** | Control Rig parameters | | **Rship Reflection Capture Controller** | Reflection capture settings | | **Rship Data Layer Controller** | World Partition data layer state | | **Rship Substance Controller** | Substance graph parameters | | **Rship BP Controller** | Blueprint vars + functions prefixed with `RS_` on the owner and sibling components | Deep dives below: [Material Controller](#material-controller), [Light Controller](#light-controller), [nDisplay Integration](#ndisplay-integration). For new subsystems, see [Extending: Custom Controllers](#extending-custom-controllers). ## RS_ Convention Any property or function prefixed with `RS_` is automatically registered. The prefix is stripped on the rship side, so `RS_Intensity` appears as just `Intensity`. | Declaration | Rship result | | ------------------------------------------------ | --------------------------------------- | | `float RS_Brightness` | Action + Emitter named `Brightness` | | `FVector RS_WorldPosition` | Action + Emitter named `WorldPosition` | | `void RS_PlayCue(FName Cue)` | Action `PlayCue` with field `Cue` | | `void RS_TriggerEffect()` | Action `TriggerEffect` (no params) | | `DECLARE_DYNAMIC_MULTICAST_DELEGATE(RS_OnHit)` | Emitter `OnHit` (event delegate) | Set `bRequireRSPrefix = false` on the BP Controller to expose every property and function on the actor (use sparingly — easy to flood rship with noise). ### Post-Write Apply A property-backed action writes the UPROPERTY's memory directly. Blueprints cannot observe that. A Blueprint reacts to a Details-panel edit only because the editor fires `PostEditChangeProperty`, which reruns the Construction Script; nothing equivalent happens for a direct write, and Construction Scripts never rerun outside the editor. A Blueprint that reads its variables live is unaffected, but one that pushes a value into components, materials, or an external system in an explicit apply step will show the new value in the Details panel while the scene stays stale. `PostWriteApply` names the apply step. It lives on `URshipPropertyApplyController`, the base the BP Controller shares with any other controller that needs an apply step (`RshipUDS`'s controller derives from it directly, without the prefix scan). Each rule is a parameterless function plus an optional list of property names that arm it: | Field | Meaning | | ------------ | ----------------------------------------------------------------------------- | | `Properties` | Written property names that arm the rule. Empty arms it on any scanned write. | | `Function` | Parameterless function on the scanned actor — Blueprint function, custom event, or native `UFUNCTION`. | Rules are armed by the write and flushed on the next tick, so a value streamed at framerate costs one apply call per frame rather than one per write. Scope rules by property name when the Blueprint offers a narrow apply function — the coarse one usually reapplies everything. Rules fire in declaration order. A function that cannot be resolved, or that takes parameters, is reported once and skipped. Property names are the UPROPERTY names, which for Blueprint variables include spaces and the `RS_` prefix (`RS_Bottom Altitude`, not `Bottom Altitude`). ## Supported Property Types | UE Type | Rship Type | Notes | | ----------------------------- | ---------- | --------------------------- | | `bool` | boolean | | | `int32`, `int64` | integer | | | `float`, `double` | number | | | `FString`, `FName`, `FText` | string | | | `FVector` | object | `{x, y, z}` | | `FRotator` | object | `{pitch, yaw, roll}` | | `FTransform` | object | full transform | | `FLinearColor`, `FColor` | object | `{r, g, b, a}` | | `FVector2D` | object | `{x, y}` | | Enum | string | enum value name | | `UObject*` | string | object path | | `TArray` | array | of supported types | | `USTRUCT` | object | nested structure | ## Organizing Targets `URshipActorRegistrationComponent` exposes: - **Category** — string label that groups targets in the rship UI (e.g. `"lighting"`) - **Tags** — array of strings for filtering and bulk selection (e.g. `["stage-left", "moving-head"]`) ## Configuration **Project Settings > Game > Rocketship Settings:** | Setting | Default | Description | | -------------- | ----------- | ------------------------- | | Host | `localhost` | rship server address | | Port | `5155` | WebSocket port | | Auto Connect | `true` | connect on play | | TCP No Delay | `true` | disable Nagle's algorithm | | Auto Reconnect | `true` | reconnect on disconnect | ## Console Commands ``` Rship.BenchmarkWebSocket [MessageCount=500] [MessageSizeBytes=65536] ``` Sends mock data to measure raw WebSocket throughput. ## Extending: Custom Controllers To expose a new subsystem, subclass `URshipControllerComponent` and override `RegisterOrRefreshTarget()`. Resolve a child target with `ResolveChildTarget(Suffix, Default)` and chain the fluent registration API to add actions and emitters: ```cpp UCLASS(ClassGroup = (Rship), meta = (BlueprintSpawnableComponent, DisplayName = "My Thing Controller")) class UMyThingController : public URshipControllerComponent { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rship") FString ChildTargetSuffix = TEXT("mything"); private: virtual void RegisterOrRefreshTarget() override { TSharedPtr Target = ResolveChildTarget(ChildTargetSuffix, TEXT("mything")); if (!Target) return; UMyThingComponent* Thing = GetOwner()->FindComponentByClass(); if (!Thing) return; Target ->AddPropertyAction(Thing, TEXT("Intensity")) ->AddPropertyAction(Thing, TEXT("Mode")) ->AddCallbackAction(Thing, TEXT("Pulse"), [WeakThing = TWeakObjectPtr(Thing)] (AActor*, const TSharedRef& Data) -> bool { if (auto* T = WeakThing.Get()) { T->Pulse(Data->GetNumberField(TEXT("Strength"))); return true; } return false; }, /*Schema*/ MakeShared>()); } }; ``` The base class handles registration lifecycle, parent-target resolution, and reentrancy. Drop your component onto any actor that already has an Rship Actor Registration — your sub-target shows up alongside the built-in controllers. ## Common Pattern: Animated Light Fixture ```cpp UCLASS() class AMyLight : public AActor { UPROPERTY(VisibleAnywhere) URshipActorRegistrationComponent* RshipTarget; UPROPERTY(VisibleAnywhere) USpotLightComponent* SpotLight; UPROPERTY(EditAnywhere, BlueprintReadWrite) float RS_Intensity = 1.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite) FLinearColor RS_Color = FLinearColor::White; UPROPERTY(EditAnywhere, BlueprintReadWrite) float RS_Pan = 0.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite) float RS_Tilt = 0.0f; virtual void Tick(float DeltaTime) override { SpotLight->SetIntensity(RS_Intensity * 10000.0f); SpotLight->SetLightColor(RS_Color); SetActorRotation(FRotator(RS_Tilt, RS_Pan, 0)); } }; ``` ## Material Controller The `URshipMaterialController` component is the bridge between rship and Unreal materials. At register time it walks the owner's mesh slots, introspects each `SourceMaterial` to discover every scalar/vector/texture parameter, eagerly creates Material Instance Dynamics on the mesh, and builds a child rship target tree — one sub-target per unique source material, one action per parameter. Parameter writes resolve their MIDs at invocation time, so the bindings survive editor/PIE/world transitions without dangling references. On top of per-parameter `Set` actions, the controller adds one batch operation: **variants** (named alternate materials per slot, swap via `SetMaterial` while preserving parameter state). ``` MaterialSlots[] rship target tree ├─ SlotIndex ───────────────── ├─ SourceMaterial ──► DiscoverParams ──► material/ └─ Variants{} ├─ M_BaseSlab/ │ ├─ Set ──► MID.SetXParameterValue │ └─ SetMaterial ──► ApplyVariant (swap MID base) └─ / ``` Each material parameter becomes a rship action; each variant becomes a `SetMaterial` choice. ## Uber Material Controller The uber-material flavor of the controller (texture-set / role-binding surface for materials namespaced by `Slab{N}_Mat{M}_*`) lives in a separate plugin — `RshipUberMaterial` — which also ships the reference `M_UberMaterial` / `MI_UberMaterial_Opaque` assets. See [Plugins/RshipUberMaterial/README.md](../RshipUberMaterial/README.md) for the controller surface, texture-set semantics, and the asset bundle. ## Light Controller `URshipLightController` is a thin rship surface over a `ULightComponent` on the owner actor. Add it to any actor that has a light component and it auto-discovers the light type and exposes the relevant properties as rship actions — no spawning, no compositing, stateless. Common properties exposed for every light type: `Intensity`, `LightColor` (RGB), `Temperature`, `bUseTemperature`, `CastShadows`, `IndirectLightingIntensity`, `VolumetricScatteringIntensity`, `bAffectsWorld`. Type-specific properties: | Light type | Extra properties | | -------------------------- | ----------------------------------------------------------------------- | | Local (Point, Spot, Rect) | `AttenuationRadius` | | Point | `SourceRadius`, `SoftSourceRadius`, `SourceLength` | | Spot | `InnerConeAngle`, `OuterConeAngle` | | Rect | `SourceWidth`, `SourceHeight`, `BarnDoorAngle`, `BarnDoorLength` | | Directional | `LightSourceAngle`, `LightSourceSoftAngle`, `bUsedAsAtmosphereSunLight` | Toggle `bIncludeCommonProperties` / `bIncludeTypeSpecificProperties` to control which buckets get registered. ## nDisplay Integration When the nDisplay plugin is enabled, the subsystem automatically discovers `ADisplayClusterRootActor` in the world (editor or runtime) and registers viewpoint targets. **Target hierarchy:** ``` :ndisplay └── viewpoints └── (one per UDisplayClusterCameraComponent) └── Action: Location (float X, Y, Z) ``` **Currently exposed:** - `Location` — sets the viewpoint component's relative position (`SetRelativeLocation`) **Available on `UDisplayClusterCameraComponent` but not yet exposed:** - `SetInterpupillaryDistance(float)` / `GetInterpupillaryDistance()` — stereo IPD - `SetSwapEyes(bool)` / `ToggleSwapEyes()` — eye swap - `SetStereoOffset(EDisplayClusterEyeStereoOffset)` — None, Left, Right - `TargetCameraType` — None, ActiveEngineCamera, ICVFXCameraComponent, ExternalCineCameraActor - `ExternalCineCameraActor` — soft reference to a CineCameraActor - `bFollowCameraPosition` — use camera position instead of root actor position - `bEnablePostProcess` — apply PP from referenced camera - `bEnableNearClippingPlane` — use NCP from referenced cine camera - `bEnableDepthOfField` — enable DoF PP settings - `bEnableICVFXDepthOfFieldCompensation` — DC DoF settings from ICVFX camera - `bEnableICVFXColorGrading` — DC color grading from ICVFX camera - `bEnableICVFXMotionBlur` — DC motion blur from ICVFX camera **Console variables:** - `r.Rship.NDisplay.RenderDomainRefreshIntervalSeconds` (default 1.0) — how often to rebuild render domain cache - `r.Rship.NDisplay.RenderDomainPublishIntervalSeconds` (default 0.25) — how often to publish render domain metadata ## Troubleshooting | Issue | Solution | | ---------------------- | --------------------------------------------------------------------------------------- | | "Not connected" | Check Host/Port settings; ensure rship server is running | | Target not appearing | Verify Target Id is set; check for duplicate IDs | | Actions not working | Ensure property is `BlueprintReadWrite` | | Pulses not sending | Verify the property's value is actually changing | | High latency | Confirm `bTcpNoDelay=true`; check rate limiter metrics | ## Modules | Module | Type | Description | | --------------- | ------- | ------------------------------------------- | | RshipExec | Runtime | Core executor, WebSocket, targets, bindings | | RshipExecEditor | Editor | Status panel, outliner column | --- slug: /pulse-unreal/docs/systems/deformers title: Deformers source: pulse-unreal --- # Deformers The deformer system uses an Optimus compute kernel to drive per-vertex deformation and material masking on a single skeletal mesh. Control flows through N stackable layers that select subsets of the geometry and apply transforms, morph weights, and material contributions. Layers and their parameters are exposed as rship targets for realtime external control. The RshipRotundaMullion plugin defines Optimus Data Interfaces to bridge C++ and HLSL. Each DI is a contract for mirroring byte-aligned structs, function declarations, and constants. All DIs are wired into the same Deformer Graph asset, giving the custom Optimus kernel access to geometry data, control layers, and mask render targets. ``` Houdini bake ├──► mullions.csv ──────────────► geometry buffer ──┐ ├──► atoms.csv ──────────────► geometry buffer ──┤ └──► skeletal mesh │ ├─ UV0: atom face uv │ ├─ UV1: per-vertex MaskRT texel address │ └─ UV2: encoded MullionID + AtomID │ ▼ Layers[N] ──────────────► Optimus kernel ──┬──► deformed verts → mesh ├──► MaskRTs → material └──► per-mullion rigid transform → lights ``` Each layer = Selection (which atoms) + Transform / Morph / Material Adjust (what to do) + Weight × Field (how much). Layers stack sequentially with blend modes. --- slug: /pulse-unreal/docs/systems/fields title: Fields source: pulse-unreal --- # Fields A **field** is the sum of contributions from a set of **effectors** (waves, noise, forces) inside a bounded domain. The same effector set feeds four evaluation paths: - **Global atlas** — compute shader bakes the full field into atlas render targets once per frame, broadcast to many consumers. - **Local atlas** — same compute path, but bakes a tighter per-mesh atlas (mesh-bounds domain). Used by material samplers for higher effective resolution near the surface. - **Inline analytical** — consumer's shader sums effector contributions directly per sample point, with optional per-sample effector mask. No texture read. - **Point analytical** — dedicated GPU point-sample pass with CPU readback for sparse exact samples. ``` Effectors ┌──► global atlas ──► Niagara, raw sampler, default materials ├─ Waves (radial / orbital) │ ├─ Noise (value / simplex / curl)├──► local atlas ──► material sampler (local-atlas mode) └─ Forces (attract / repel) │ ├──► inline solve ──► Optimus DI (mullion field gating, mask-aware) │ └──► point sample ──► light sampler (CPU readback) ``` Each effector = `Position + Radius` + type-specific shape + `Amplitude × Falloff` + `TransportId`. **Transports** are named time sources owned by the field — each can be paused, run at its own rate, or driven externally. All effectors bound to the same transport stay phase-coherent; multiple transports let independent groups of effectors run on independent timelines. **Traveling wave effectors emit wavefronts**, each one a snapshot of the effector's scalar pulse-shape state (`WaveSpeed`, `Attack`, `Release`, `Amplitude`, `Falloff`, `Waveform`) at emission time. Changing those parameters between pulses only affects subsequently-emitted wavefronts — already in-flight ripples keep propagating with the values they were born with, so live parameter edits don't snap existing pulses. (`VectorDirection` is the one exception — it's read live so all active wavefronts share the effector's current direction.) --- slug: /pulse-unreal/docs/systems/lights title: Lights source: pulse-unreal --- # Lights ## URotundaMullionLightController `URotundaMullionLightController` is a rig that spawns and drives a structured grid of rect lights bound to the mullion mesh. Five slots per mullion (`Top`, `Bottom`, `Left`, `Right`, `Face`) × 81 mullions = 405 `ARectLight` actors. At spawn time it computes a rest-pose transform per slot from the static mullion geometry. Each tick it reads back the deformed per-mullion transforms published by the deformer kernel (`URotundaMullionComponent::GetDeformedMullionTransforms`) and re-poses every light through `Deformed.TransformPosition(Rest.Location)` so the lights follow the deformation exactly. Light state is layered on top of the spawned grid: N stackable `FMullionLightLayerDesc` layers, each selecting a mullion subset (range string) and slot subset (per-edge bools), composited via `Mute / Solo / Weight` into a `FMullionLightResolvedState` per light. Each property (Intensity, Color, Temperature, BarnDoorAngle, TiltAngle) carries a `bSet` flag so layers only contribute the props they care about. Per-layer `FieldId` opts the layer into a bulk field sample (one analytical point sample per light, via `IRshipFieldBulkSampleProvider`) so a layer can modulate by field value at the light's own position. Applied state is cached per light to skip redundant render-thread setters when nothing changed. ``` spawn: N × 5 ARectLights + RestPoseLightTransforms[] (once) per tick — two independent inputs per light: Deformer.GetDeformedMullionTransforms() × RestPose ──┐ │ ├──► ARectLight │ (transform + props, Layers[N] ──► CompositeLayers ──► resolved state ────┘ skip redundant setters) │ ├─ Selection (mullions + slots) ├─ Mute / Solo / Weight ├─ bSet + values (Intensity, Color, Temp, BarnDoor, Tilt) └─ FieldId ──► bulk point sample ``` Each layer = mullion + slot selection (where) + opt-in property sets (what) + Weight × FieldId modulation (how much). Layers composite sequentially into a single resolved state per light. ## URshipLightController `URshipLightController` is a thin rship surface over a `ULightComponent` on the owner actor. Add it to any actor that has a light component and it auto-discovers the light type and exposes the relevant properties as rship actions — no spawning, no compositing, stateless. Common properties exposed for every light type: `Intensity`, `LightColor` (RGB), `Temperature`, `bUseTemperature`, `CastShadows`, `IndirectLightingIntensity`, `VolumetricScatteringIntensity`, `bAffectsWorld`. Type-specific properties: | Light type | Extra properties | |---|---| | Point | `AttenuationRadius`, `SourceRadius`, `SoftSourceRadius`, `SourceLength` | | Spot | `InnerConeAngle`, `OuterConeAngle` | | Rect | `SourceWidth`, `SourceHeight`, `BarnDoorAngle`, `BarnDoorLength` | | Directional | `LightSourceAngle`, `LightSourceSoftAngle`, `bUsedAsAtmosphereSunLight` | Toggle `bIncludeCommonProperties` / `bIncludeTypeSpecificProperties` to control which buckets get registered. --- slug: /pulse-unreal/docs/systems/materials title: Materials source: pulse-unreal --- # Materials Two halves: **`URshipMaterialController`** (C++ bridge in `RshipExec` — works with any UE material) and **`M_UberMaterial`** (the reference Substrate material the controller is designed to drive). The controller is plugin code; the material is a project-authored `.uasset` following the convention below. --- ## URshipMaterialController Walks the owner's mesh slots at register time, introspects each `SourceMaterial` for scalar/vector/texture parameters, eagerly creates MIDs, and builds a child rship target tree — one sub-target per unique source material, one action per parameter. Parameter writes resolve their MIDs at invocation time, so bindings survive editor/PIE/world transitions. ``` MaterialSlots[] rship target tree ├─ SlotIndex ───────────────── ├─ SourceMaterial ──► DiscoverParams ──► material/ └─ Variants{} ├─ M_BaseSlab/ │ ├─ Set ──► MID.SetXParameterValue │ └─ SetMaterial ──► ApplyVariant (swap MID base) └─ / TextureRoles + TextureSets ──► AssignTextureSet(Slab, Mat, SetName) │ │ role pattern ─┐ ▼ scope filter ─┴──► params matching BOTH ──► MID.SetTextureParameterValue ({Slab}/{Mat}) RescanTextureSets: /Game/VTex//* ──► TextureSet[] (filename → role via FilenamePatterns) ``` On top of per-parameter `Set` actions: **variants** (named alternate materials per slot, `SetMaterial` swaps preserving param state) and **texture sets** (role→texture bundles applied via `AssignTextureSet(Slab, Mat, SetName)`, scoped by glob patterns on role name and `{Slab}/{Mat}` placeholders). `RescanTextureSets` auto-populates from a content folder. `ResetTextureRoleToDefault(RoleName)` writes the parent material's authored default back to the MID for every matching texture param — generic, used for transitions, fault recovery, and producer hot-swap. --- ## URshipStandaloneMaterialController For cases where a material lives on **another actor's `UPROPERTY`** rather than on a mesh slot the controller owns — most often a third-party plugin actor whose material can be swapped from the outside (e.g. a sky system's custom sky sphere material) — `URshipStandaloneMaterialController` is the variant that drives that pattern. It does not walk mesh slots; it targets one named material reference on a configured `TargetActor` and writes through a live MID held on a sibling property. ``` TargetActor ├─ MaterialProperty (FObjectProperty) ◄── AssignMaterial(name) └─ MIDProperty (FObjectProperty) ◄── //Set ``` Configuration on the component: | Property | Purpose | |---|---| | `Materials` | The catalog of materials this controller can assign. `AssignMaterial(name)` picks one by name. | | `TargetActor` | The actor whose properties to read and write. Null → defaults to `GetOwner()`. | | `MaterialProperty` | Name of the `UMaterialInterface*` UPROPERTY on `TargetActor` where `AssignMaterial` writes. | | `MIDProperty` | Name of the `UMaterialInstanceDynamic*` UPROPERTY on `TargetActor` where parameter `Set` actions write. | | `ChildTargetSuffix` | rship target group suffix (defaults to `"material"`). | rship surface: - `AssignMaterial(name)` — writes one of `Materials` into `MaterialProperty`. - `ClearMaterial()` — nulls `MaterialProperty` (reverts to the foreign actor's built-in default). - `//Set` — writes into the live MID at `MIDProperty`. The property names are configured at the foreign actor's UPROPERTY level, not by code-level binding — different third-party actors expose their custom-sky / custom-material slot under different property names. `LogCandidateProperties` (CallInEditor) dumps every `UMaterialInterface*`-typed property on the resolved target actor to the Output Log, so the right `MaterialProperty` / `MIDProperty` names can be picked off the log rather than guessed. This is the controller to reach for when the goal is **"swap and parameterise the material on someone else's actor"** rather than "drive a material on a mesh I own". `URshipMaterialController` remains the right tool when the material lives on the controller's owner mesh slots. --- ## M_UberMaterial — graph structure 3 Substrate BSDF slabs (`Slab0`, `Slab1`, `Slab2`), composed flat at the master graph level. All three are backed by PBR texture sets. A custom shader graph (when present) feeds INTO one or more of these slabs' BSDF inputs between the slab's PBR texture parameters and `MakeMaterialAttributes` — it provides procedurally-computed `BaseColor` / `Normal` / `RMA` / `Height` for that slab rather than introducing a fourth closure. From the blend chain's perspective the custom-shader path is invisible; the chain sees 3 BSDF inputs. ``` Slab{N}_{D,N,RMA,H} texture params N ∈ {0, 1, 2} │ │ (custom shader graph may sit between │ the slab's PBR texture parameters and MakeMaterialAttributes │ on any slab, providing procedural inputs) ▼ Substrate Slab BSDF (× 3) (one PBR variant per slab; minimal feature set) │ ▼ Substrate Horizontal Blend chain (2 mixes, share-fraction weights) │ weight = AdjustTexture.r × AdjustScalar (per slab, saturated) ▼ Substrate Coverage Weight (coverage = saturate(w0+w1+w2)) ▼ Final Material Output ``` Black fill is **implicit** — the Coverage Weight node scales the BSDF lit contribution by total coverage; sub-1 totals fade toward black. Adds 0 closures, 0 bytes (`HLSLMaterialTranslator.cpp:13580-13585`). All 3 slabs are symmetric. ### Texture inputs | Texture object param | Count | Purpose | |---|---|---| | `Slab{N}_BaseColor` / `_Normal` / `_RMA` / `_Height` (N ∈ 0..2) | 12 | PBR data for the three slabs. Bound by `URshipMaterialController` per texture set. Slabs whose inputs are computed by a custom shader graph still expose these params for the cases where the artist wants to fall back to texture sampling. | | `Slab{N}_AdjustTexture` (N ∈ 0..2) | 3 | Per-slab single-channel weight texture from the deformer producer. Default = engine white (1.0 everywhere). | Plus 3 `Slab{N}_AdjustScalar` scalars (default 1.0) — the live-control knob, exposed as rship `Set` actions. PBR-slab param total is 18 (12 textures + 3 AdjustTexture + 3 AdjustScalar). ### Weight chain (per slab) ``` Slab{N}_AdjustTexture ── Sample(UV1).r ──┐ × ── Saturate ──► slab N contribution scalar Slab{N}_AdjustScalar ────────────────────┘ ``` UV1 (TexCoord[1]) is hardcoded to match the deformer producer's LUT convention. 1×1 default white textures sample uniformly regardless of UV channel, so non-producer-driven slots don't need to care. A Vertex Interpolator pins the sample to vertex rate. ### Blend math `w0..w2` are the per-slab contribution scalars. Two horizontal mix nodes chain the slabs by share-fraction weights, then one Coverage Weight scales the result by total coverage: ``` horz mix 1: Slab0 ⇄ Slab1 weight = w1 / (w0 + w1) horz mix 2: mix1 ⇄ Slab2 weight = w2 / (w0 + w1 + w2) coverage: saturate(w0 + w1 + w2) (wraps mix 2) ``` Convention: `Background ⇄ Foreground`, weight 0 → Background. Each scalar acts as that slab's individual contribution; content slabs compose by relative share. | Inputs | Coverage | Final result | |---|---|---| | All 0 | 0 | **Black** | | `w2=0.5` only | 0.5 | 50% Slab2, 50% black | | `w0=w1=w2=⅓` | 1.0 | Equal 3-way mix | | All ≥ 1 | 1.0 (saturated) | Sum-normalized shares | **Numerical safety**: `w1/(w0+w1)` is `0/0` when both are zero. Coverage Weight zeroes the all-zero case so NaN doesn't reach the framebuffer, but mixed cases (e.g. `w0=w1=0, w2=1`) still hit `0/0` in mix 1. Add an epsilon or guard each weight with `(denom > 0 ? num/denom : 0)`. ### Substrate budget - **Closures: 3** (one per slab). Well within `r.Substrate.ProjectClosuresPerPixel = 4` (`RenderUtils.cpp:1976`). Coverage Weight adds 0 closures. - **Bytes per pixel**: ~63 baseline (3 × ~20 + 3 shared local basis). Cap is 256 (`RenderUtils.cpp:2116`). Each plugged-in slab feature (F90/Fuzz/Anisotropy/SSS/MFP/SpecProfile) adds 4 bytes per slab; Glint adds 16. **Keep slabs to BaseColor/Normal/RMA/Height only** unless a slab actually needs a feature. - **Operators**: 3 BSDFs + 2 mixes + 1 Coverage Weight = 6 of 15 (`SubstrateDefinitions.h:22`). - **Lighting cost**: identical to a 3-BSDF horizontal mix. An explicit black BSDF would cost +33% per-light-per-pixel (`SubstrateDeferredLighting.ush:108`) — landing on every nDisplay pixel. Coverage Weight avoids this. ### Producer-side routing (plugin) The deformer producer exposes a per-layer `SlabIndex` (enum `Slab0`/`Slab1`/`Slab2`) instead of per-channel mask adjusts. Each layer's `saturate(Weight) × gates × edgeProfile` accumulates additively into one slab's weight RT — the slab the layer's `SlabIndex` points at. Cross-slab mixing at the same selection comes from authoring multiple layers each targeting a different slab. The producer writes 3 single-channel R16F render targets (one per slab) and pushes them under `Slab0_AdjustTexture` / `Slab1_AdjustTexture` / `Slab2_AdjustTexture` to every MID the artist's `URshipMaterialController` created from the source material. The artist's material samples them at UV1 with vanilla `Texture Sample Parameter` nodes. ### Custom shader inputs A custom shader graph can be wired between the slab's PBR texture parameters and `MakeMaterialAttributes` on any slab. It does NOT register as its own slab — the closure count stays at 3, the blend chain stays at 2 mixes. The custom graph reads/computes whatever data it needs (per-vertex inputs, parameters, procedural noise, etc.) and outputs the slab's `BaseColor`/`Normal`/`RMA`/`Height` directly into that slab's BSDF. Any scalar / vector / texture parameters the custom graph exposes are discovered by `URshipMaterialController` and surfaced as `Set` actions like any other slab parameter. --- ## Producer integration Slab weight textures are the producer-side hook — any system driving spatial slab activation writes single-channel textures and assigns them via the controller's MID: ```cpp MID->SetTextureParameterValue(FName("Slab0_AdjustTexture"), MyWeightRT); ``` These aren't TextureSet-shaped (single role per param, control data not bundled PBR), so producers assign the RT directly rather than via `AssignTextureSet`. | Scenario | `AdjustTexture` source | `AdjustScalar` source | Behavior | |---|---|---|---| | Production deformer | Producer writes weight RTs (per-layer `SlabIndex` 0..2, additive within slab) | rship default 1.0 | spatial producer control + live override | | Test (no producer) | Authored white default | rship-driven 0..1 | scalars drive uniform activation | | Hybrid | Producer-driven spatial | rship-driven 0..1 | producer = where, rship = global gate | | Reset mid-session | (any) | (any) | `ResetTextureRoleToDefault("SlabWeight")` snaps to white; producer re-overrides next frame | See [deformers.md](deformers.md) for the deformer kernel side. --- ## Material authoring gotchas ### Usage flags must be baked into the saved asset The editor auto-adds a missing usage flag on first use and renders correctly; `-game` and cooked builds do not, and substitute the Default Material for the whole surface. A deformer-driven skeletal mesh requires `MATUSAGE_MeshDeformer` rather than `MATUSAGE_SkeletalMesh`, and every static-mesh section requires `MATUSAGE_StaticMesh`. Mechanism, the full consumer-to-flag table, and why PIE does not catch it: [editor-vs-game-divergence.md](../editor-vs-game-divergence.md#material-usage-flags). ### Virtual texture sampler types `Texture Sample Parameter` only exposes `Virtual Color` / `Virtual Linear Color` sampler types when the **default texture asset is itself a VT**. Reset the default to a non-VT texture (happens silently when copying or duplicating a material) and UE removes the Virtual options from the dropdown *and* downgrades the existing setting to non-VT. The parameter looks correct in Details, but every sampler reading it is compiled non-VT — runtime `MID->SetTextureParameterValue(name, VT)` no longer propagates. **Symptom**: bind a runtime VT (`URshipSubstanceVirtualTexture`, UDIM stream) to a MID, observe nothing. The "expects texture to be Virtual" warning only fires when the *param* is still VT-typed and the bound asset isn't — silent downgrade gives no warning at all. **Fix**: set every Texture Sample reading the param back to `Virtual Color` / `Virtual Linear Color`. If those aren't in the dropdown, the param's default needs `Virtual Texture Streaming = true` first. Always re-verify Virtual sampler type after editing defaults — copying or duplicating a material with VT slab params is a common path into this bug, and the silent downgrade leaves no warning. ### Engine-shipped VT defaults Use UE 5.7's `/Engine/EngineMaterials/` flatten textures as parameter defaults so the material compiles VT-typed without project-authored placeholders. | Asset path | Recommended for | |---|---| | `Black_1x1_EXR_Texture_VT` | universal 1×1 placeholder (linear) | | `BaseFlattenDiffuseMap_VT` | sRGB color (BaseColor) | | `BaseFlattenLinearColor_VT` | linear RGB (RMA, packed channels) | | `BaseFlattenNormalMap_VT` | normal maps | | `BaseFlattenGrayscaleMap_VT` | single-channel (Height, weights, AdjustTexture) | | `BaseFlattenEmissiveMap_VT` | emissive | For `M_UberMaterial` slab params: `_BaseColor → BaseFlattenDiffuseMap_VT`, `_Normal → BaseFlattenNormalMap_VT`, `_RMA → BaseFlattenLinearColor_VT`, `_Height → BaseFlattenGrayscaleMap_VT`, `_AdjustTexture → BaseFlattenGrayscaleMap_VT`. See [../virtual-textures.md](../virtual-textures.md) for show-mode pool sizing. --- ## Open questions - 3 PBR slabs. If a project needs more, options are (a) add more slabs (each is +1 closure, +20 bytes baseline; closure budget can be raised to 8 max via `r.Substrate.ProjectClosuresPerPixel`, hard cap at `SubstrateDefinitions.h:21`), or (b) reintroduce a per-slab variant blend as a Substrate parameter-blend (one BSDF, lerped inputs). --- slug: /pulse-unreal/docs/systems/sky title: Sky source: pulse-unreal --- # Sky Two material-driven HLSL passes stacked. The snippets in [pulse-unreal](https://github.com/ignition-is-go/pulse-unreal) are the source of truth; they live embedded in two material assets: - **`M_RTGradient`** ← `sky_gradient_11point.hlsl` — sky base colour to a 2D RT. - **`M_Sky`** ← `sky.hlsl` (or `sky_dev.hlsl` warp-only variant) — reads the RT as `Environment` and produces the final sky pixel. ``` Gradient control points (P0..P10) Sun dir + color, Camera pos │ │ ▼ ▼ M_RTGradient ──► Environment RT ──► M_Sky ──► final sky pixel (polyline + (sky base) (volumetric raymarch nearest-segment over gyroid noise, color lerp) self-shadowed against sun; warp variant skips the raymarch and just warps UV into Environment) ``` **Gradient**: 11 control points define a polyline in UV space, each with an RGB colour. Each pixel projects onto its nearest segment and lerps the two endpoint colours by `t` — so the gradient follows an arbitrary curve through UV space, not just a strip. **Sky**: marches along the pixel's world normal through a density field built from gyroid noise rotated by golden-ratio matrices. At each step, samples density at the point and at a sun-offset point to derive shadow, samples the gradient RT in spherical UV for lit colour, accumulates `Environment(polar(p)) × lerp(Shade, Sun, light)`. Exposure: `1 - exp(-Brightness × col / STEPS)`. `sky_fixed.hlsl` corrects the polar mapping axes; `sky_dev.hlsl` is the simple warp-only fallback. Each shader declares its inputs via header comments (`Time`, `SunDirection`, `SunColor`, control points, etc.) — these are pin names on the material's Custom node. Whatever the material asset wires into them (engine time, ToD driver, rship actions, artist keyframes) is what the shader sees. --- slug: /rship-touchdesigner/readme title: rship-touchdesigner source: rship-touchdesigner --- # Rship-TouchDesigner The TouchDesigner executor for Rocketship. Establishes a connection with a Rocketship server, scans the network for Base COMPs with an 'rship' tag, and exposes their custom parameters to rship as targets. Notch TOPs also have first-class support. Similar to Base COMPs, their parameters can be quickly turned into targets by tagging the Notch TOP 'rship'. ## Setup 1. Download the rship.tox and drag it into a network > NOTE: When scanning the network for targets, the rship.tox recursively scans the directory it is in and all subdirectories. Place the rship .tox in the root of the network to ensure that the entire network is scanned (also to avoid cluttering the screen with reference lines). 2. In the rship .tox parameters page, enter the address and port of a Rocketship server (default 5155) 3. Save the project 4. Verify the TouchDesigner instance appears in the rship GUI, and activate the instance 5. Tag a Base COMP 'rship' and save the project 6. Verify the COMP appears as a target in the rship GUI ## Expose Parameters 1. Open the component editor window of a tagged COMP 2. Click and drag the par to be exposed onto the component editor window 3. See [Component Editor Dialogue](https://derivative.ca/UserGuide/Component_Editor_Dialog#:~:text=Bind%20New%20Par%20as%20Master,the%20parameter%20that%20was%20dragged.) to understand the different kinds of bindings/references in TouchDesigner 4. Save the project 5. Verify the parameter appears in the rship GUI as a target > NOTE: As of 2025/03/21 Rship-Touchdesigner currently supports all primitive data types. COMPs/OPs and Python are currently unsupported. > Note: The unique ID for a parameter is based on its **name**. Changing its **name** will break the connection and create a new target in rship. Prefer renaming labels. ## Use Notch TOPs 1. Tag a Notch TOP 'rship' and save the project 3. Verify the Notch layers and their parameters appear in the rship GUI as targets ## Targets, Actions, and Emitters - Targets: Base COMPs - Emitters: - Publish custom par values - Actions: - Activate cooking - Deactivate cooking - Set custom par values - Targets: Notch TOPs - Emitters: - Publish Notch par values - Actions: - Activate cooking - Deactivate cooking - Set Notch par values - Targets: Notch Layers - Emitters: - Publish layer par values - Actions: - Set layer par values ## Legacy properties and loading The Python source pairs supported ordinary parameter Set actions with their existing Updated emitters using legacy `Action.writesTo` metadata. It does not use the server Views schema. Parameter target IDs, Set and Resend action IDs, and Bulk Set payloads remain compatible with existing scenes. A property setter publishes the parameter's actual value after assignment, including unchanged or normalized values. The executor also supplies current values on registration, reconnect, and server `ResendEmitterValue` requests. Menu values use `menuNames`; labels only control display. Scalar values keep the existing `{"value": ...}` shape, and vectors retain their component objects. For chain scenes, select and elect the desired properties in Rship. The executor does not convert existing scenes or elect property nodes automatically. Sequences expose a writable ` State` property containing an array of blocks. The array length sets the block count within TouchDesigner's limits, with at least one block. Empty arrays and arrays above a sequence's maximum are rejected before assignment. Each block contains its persistent parameter values, including Pulse and Momentary members represented by the canonical `format: "exec-tick"` object (`id`, `prev`, and `next`). Nil and previously handled IDs do not fire, so property reconciliation cannot replay Emit or Clear events. A real local TouchDesigner pulse creates a fresh UUIDv4 and the most recent ID is retained for snapshots, resends, and unrelated value updates. Ordinary controls on sequence-based operators also support properties. Add the `rship-no-properties` tag to an operator to disable writable pairing explicitly. Existing emitters remain registered, so the legacy server may still display them as read-only properties. The executor publishes Starting before scanning and registering targets. It publishes definitions and current value seeds before target Online statuses and instance Available. Early action and Resend requests wait in arrival order while registration finishes. Failed scans or sends retry through the existing timer with delays capped at 30 seconds. Disconnect clears deferred commands and pending outbound pulses. The loading queue accepts at most 1,024 command envelopes and 16 MiB of text. Overflow returns a command error. Queue draining yields after 16 envelopes or 4 milliseconds and resumes on the next frame. A single batch envelope executes as a whole, so an unusually large batch can exceed that frame budget. The bundled `rship.tox` includes these changes. The development `RshipTox.toe` references that TOX. Automated behavior tests use simulated TouchDesigner parameters and real protocol serialization: ```sh python -m unittest discover -s tests -v ``` Rebuild the package with the installed TouchDesigner utilities: ```powershell python tools/build_tox.py --td-bin "C:/Program Files/Derivative/TouchDesigner.2025.33070/bin" --output rship.tox ``` Omit `--output` to check the existing package. The builder embeds UTF-8 source with LF newlines, re-expands the output, and verifies every embedded Python DAT against source. It also checks the table of contents and confirms that all other package data remains unchanged. Validation on 2026-09-09 passed all 53 automated tests. Isolated TouchDesigner 2025.33070 checks covered loading, ordinary properties, and 17 sequence checks, including embedded-code reload, block resizing, minimum-block rejection, event filtering, current readback, and emitter fanout. Network output was captured locally. The installed production component registered 31 sequence properties and returned to Active without operator errors. Production server scene reconciliation still requires an end-to-end check after installation. The protocol reference is the [pre-Views property implementation](https://github.com/ignition-is-go/rship/blob/2888e94b3ac801a83829ff9f0215d8208c792994/libs/entities/external/src/property.rs). --- slug: /phosphor/readme title: Overview source: phosphor --- # Phosphor *Where the signal becomes light.* Phosphor is a native Vulkan media renderer controlled by [rship](https://github.com/ignition-is-go/rship)'s durable Output Mesh. It uses a Myko connection and advertises external render nodes for use by every session. It targets the essential shape of a media server: map content through logical Screens, compose native scene-linear Screen images, sample those images into completed Outputs, and fan completed frames out to physical and software receivers. Screen previews observe those same native images. SMPTE ST 2110 is the primary output contract. File decoding and web-stream previews are edge adapters; they do not define the renderer's frame model. The current design is captured in [`docs/specs/2026-05-29-phosphor-design.md`](docs/specs/2026-05-29-phosphor-design.md). ## Architecture ```text rship / Myko │ live Outputs + Screens + placements + receiver bindings ▼ phosphor-rship ── RenderPlan ──► phosphor-vulkan (Vulkan) │ completed GPU frame pool ┌─────────────┴─────────────┐ ▼ ▼ ST 2110 output driver output adapters (raw Linux development; (HTTP preview, WebRTC, ...) Rivermax future) input adapters (file decode, ...) ──► render sources ST 2110 inputs ─────► render sources phosphor-server ──► empty Myko server boundary (no local entities/persistence) phosphor-viewer ──► Phosphor raw RGBA stream + server/upstream status ``` rship owns durable render and routing intent. Outputs are the demand roots, Screens are logical canvases, each `OutputScreenPlacement` owns one pixel sample rect → pixel output rect relationship, and `ReceiverBinding` owns Output-to-receiver delivery. Duplicate Screen/Output placements are valid. `phosphor-rship` watches four independent live Myko queries and materializes an in-process `OutputMeshView`; it does not copy rship state into Phosphor entities. The authoritative Output snapshot immediately creates and retires render targets; Screens, placements, and receiver bindings enrich those targets as their snapshots become ready. `phosphor-vulkan` turns each plan into a three-image, device-local offscreen frame pool. Transport implementations consume completed frames without changing the rship model. ### Crates | Crate | Role | |---|---| | `phosphor-model` | Pure render plans, typed pixels, output intent, and viewer/control DTOs | | `phosphor-runtime` | Output scheduling, epoch-aware renderer ports, lockstep coordination, deterministic logical-tile placement, and input/output reconciliation against `st2110` contracts | | `phosphor-ocio` | Cargo-owned native OCIO build, Rust/C ABI wrapper, processor preparation and shader/LUT data; no Vulkan execution | | `phosphor-vulkan` | Vulkan device-group and independent-device execution, frame-pool reconciliation, CUDA P2P delivery gather, GPU preview export, and band-scoped completed-frame readback | | `phosphor-rship` | Raw Myko control adapter joining Outputs, Screens, placements, and receiver bindings | | `phosphor-file` | FFmpeg CUDA hardware decode, timestamp selection, seeking, GPU surface ownership, and image/reference adapters | | `phosphor-server` | Composition root selecting Vulkan and ST 2110 backends; owns HTTP preview and process configuration | | `phosphor-viewer` | Native Pulse GPUI viewer using Phosphor's live render plans, raw RGBA stream, and separate viewer/server and server/rship health indicators | ## Current implementation boundary The pivot establishes the native renderer and I/O boundaries; it does not yet claim a complete media server. Today it: - creates a Vulkan 1.3 instance and logical device; - deterministically selects the largest executable Vulkan physical-device group instead of trusting adapter enumeration order, and publishes its directional peer-memory capabilities; - treats enabled rship Outputs as the authoritative set of render feeds; - removes feeds when Outputs are disabled or disappear from authoritative state; - preserves exact Screen-to-Output and Output-to-receiver mapping geometry; - validates backend-neutral render plans; - renders native SMPTE, ball, and snow test sources through a Vulkan compute pipeline; - compiles the Vulkan program from focused WGSL interface, pattern, color, sampling, correction, label, and composition modules; - renders a labeled SMPTE fallback using the Screen name when a placed Screen has no content; - allocates and reconciles three device-local RGBA16F images per active plan; - deterministically assigns logical view tiles to compatible devices in the backend-neutral runtime, accounting for remaining memory and required local, peer, host-staged, or remote transfer paths; - partitions each full-frame Vulkan compute dispatch into non-overlapping workgroup strips across peer-compatible device-group members, retaining device-group barriers and device 0 as the current allocation/export owner; - can require a minimum number of devices to receive actual workgroups, failing both startup capacity checks and per-plan peer/residency qualification rather than silently degrading an intended multi-GPU deployment to one GPU; - resolves the Vulkan allocation owner's stable device UUID to exactly one visible CUDA ordinal before exporting a frame to NVENC; API enumeration order is never treated as GPU identity; - carries an exact logical `RenderEpoch` and PTP presentation instant through scheduled render requests, with a deterministic one-or-many-participant lockstep coordinator for release, late, failure, and expiry outcomes; - partitions an exact production UUID set into independently enumerated Vulkan backends, fans one scheduled epoch to all of them, and withholds the public frame revision unless every backend reaches the local barrier; - renders demanded roots as exact horizontal bands across independent singleton Vulkan backends, imports their delivery buffers and timeline semaphores into UUID-matched CUDA contexts, and gathers peer bands into the deterministic owner without CPU pixel staging; - reads only each independent GPU's assigned band for CPU-backed ST 2110 and native-viewer adapters, then validates and assembles one exact revision; - gates ST 2110 epochs through an injectable PTP health provider and reports `Verified` only when role, grandmaster, measured offset, observation age, and holdover satisfy the configured policy; - bounds scheduled Vulkan render and exact-revision readback waits with local monotonic deadlines derived conservatively from the PTP wire deadline, including renderer-state lock acquisition; - copies completed frames through a read-only RGBA8 boundary; - opens one live raw-frame monitor window per active Output on graphical hosts; - provides a native Pulse GPUI viewer with low-latency raw RGBA frames and truthful per-link connection status; - models ST 2110-20 formats, network legs, and ST 2022-7 redundancy directly; - resolves versioned rship assets through the SDK into a verified local cache; - plays file-node video with play, pause, seek, and loop controls through a persistent input worker shared by its consuming plans. The current Vulkan compatibility slice executes ordered plan layers into whole-frame targets; it does not yet materialize the logical canvas tile plan. Render-instance existence alone starts continuous exact rational cadence and the raw preview stream. No receiver binding, ST 2110 route, or ST 2110 process configuration is required to preview a feed. A later receiver binding may add a transport consumer without defining render ownership. The server supports explicit device-local workers with component placement. Each selected UUID owns one singleton Vulkan renderer, its decode domain and its delivery preparation domain. Connected stages and shared sources must be co-located; independent graph components can use different workers. Unanchored components, conflicting owners and unsupported cross-device dependencies reject before graph installation. This path does not replicate every stage across GPUs or perform implicit peer copies. Its singleton hardware path is tested; physical multi-GPU operation and sustained throughput remain unqualified. Scheduled ST 2110 rendering now passes an epoch-aware request into the renderer instead of reconstructing identity from a local free-running counter. The runtime lockstep coordinator validates exact epoch, presentation-time, and local-deadline echoes from a fixed participant set and never releases partial, late, or failed work. The active Vulkan device-group path executes its physical participants in one group submission and publishes only after its fence signals before the deadline. Timed-out submissions retain their command buffers, fences, and frame resources until they can be reaped; their pixels are never published. Vulkan cannot cancel an already submitted command. Device loss, permanently hung submissions, partial all-device completion, backend revision divergence, and CUDA gather failure therefore quarantine the complete local GPU set. The service runtime observes that terminal state and exits non-zero so its supervisor can recreate every Vulkan/CUDA context as one coherent set. Runtime shutdown has a bounded grace period so Tokio's non-cancellable blocking pool cannot hold the process indefinitely while that recovery takes over. An all-device release also compares the complete leaf-to-root renderer-private dependency closure: plan revisions, captured Stream/intermediate input revisions, and dirty state must agree on every GPU. Matching root counters alone is not proof of a coherent frame. Partial plan or source reconciliation quarantines the local GPU set rather than allowing its copies to drift. Creative blend stages are shared by session, authored node, canonical cadence, and complete compiled definition. Equivalent rates such as 60/1 and 120/2 share one stage; different procedural cadences remain distinct. Definition edits get new stage identities, allowing old and new outputs to coexist during a rolling update. The last owning feed retires each version. Mapping-preview identities remain output-specific. On one backend, completed intermediate revisions are published to observers without rerendering their producers. Multi-backend band stages still need an explicit gather before the same publication can be offered. Version changes can require overlapping stage storage; rapid control-update allocation cost is not yet qualified. The raw-Linux development sender materializes configured ST 2110-20 routes with CPU readback, BT.709 YCbCr 4:2:2 10-bit conversion, RFC 4175 packetization, exact TAI-derived ST 2110-21 reference times, and optional Linux `SO_TXTIME` requests. This is not the future zero-copy Rivermax path and does not claim wire compliance. The server has an honest PTP discipline gate, and the Linux backend now combines authenticated, correlated linuxptp read-only management responses with a `/dev/ptpN` to `CLOCK_TAI` cross timestamp. It verifies only an active hardware-timestamped Slave with a remote grandmaster and fresh Sync evidence. The sampler runs off the output thread and admissions read its cache; the first authoritative probe failure immediately invalidates any cached success. ST 2110 conversion, packetization, sender-memory acquisition, and per-flow plans are completed for every route before any route is released. Retryable socket or TXTIME setup failures recover on monotonic exponential backoff inside the sole per-output worker, without requiring a new Myko event and without stopping preview cadence. Kernels without the CLOCK_TAI-capable cross-timestamp ABI are rejected rather than misreported as verified. `UnverifiedTai` remains available only through an explicit development opt-in. Display/NDI transports, Rivermax, and NMOS activation remain future work. Inputs without an available frame render an explicit magenta unavailable-source slate. An empty logical Screen instead renders its labeled SMPTE diagnostic fallback. ### Asset video playback Phosphor connects as a peer render service. `PHOSPHOR_MACHINE_ID` selects its rship Machine for asset reception, falling back to `MACHINE_ID`, the host machine ID, or the hostname. Machine registration preserves existing metadata. Session execution state is separate even when two sessions use the same scene and values. The node catalog survives provider disconnection; reconnect restores live service registration and reconciles the current authoritative graph. The Phosphor node category includes Video File, Stream, Canvas, Transform 2D, Color Correct, Opacity, Over, Blend, Mapping, and Output. Connect a producer's `video` output to a unary operator's `media` input, or to Over/Blend's `background` and `foreground` inputs. Output accepts ordered `video` fan-in. Blend supports normal, add, multiply, screen, overlay, darken, and lighten. Mapping selects a Screen Mapping with the `mappingSetRef` dropdown. Its `MappingSetRef` schema asks rship to resolve the available choices. Content reaches an Output through enabled Screen Mapping placements, Screens, and Output Screen placements in the session's project. `layerOrder` controls mapping stacking; `presence` controls its weight. The generic `order` anchor controls upstream wire ordering separately. Legacy media-node reports are no longer consumed; author render graphs with the Phosphor external nodes. File nodes select an rship `AssetQuery` and expose `playing`, `positionMs`, `seekRevision`, and `loop` anchors. Phosphor selects the highest indexed version within the query's bounds. The asset index must include valid video dimensions; conflicting integrity or video metadata leaves the node unavailable until the index changes. Asset reception uses the configured Machine ID. `PHOSPHOR_ASSET_CACHE_DIR` optionally selects the durable cache root; the default is `$XDG_CACHE_HOME/phosphor/assets` or `$HOME/.cache/phosphor/assets`. Phosphor registers a receive-only SDK asset peer on its existing Myko connection. A connected asset provider must serve the selected version. Provider filesystem paths are never opened by Phosphor. The SDK streams the asset into a temporary cache file. Length and BLAKE3 checks precede durable publication, and cached bytes are verified before reuse. Missing providers and failed transfers retry with a bounded backoff. Playback starts after the complete file is available locally. Builds require FFmpeg development libraries (`libavcodec`, `libavformat`, `libavutil`, `libswscale`), pkg-config, and libclang. The decoder uses `ffmpeg-the-third` 6; current native verification uses FFmpeg 6.1.1. Video decoding requires NVDEC on the selected renderer GPU. NV12/P010 samples remain on the GPU through source reconstruction and OCIO conversion into the shared RGBA16F working image. Configured still inputs decode once and upload full-precision source samples. Their selected metadata chooses an explicit OCIO transform before RGBA16F working-image admission. Audio is not implemented. Normal startup uses the bridge built by `phosphor-ocio/build.rs` and OCIO's built-in, versioned Studio config. A regular Cargo build fetches verified pinned sources and builds OCIO, glslang and the bridge under the workspace's default `target/`. Deleting `target/` requires no manual OCIO setup before rebuilding. The host needs a C++ toolchain, CMake and Git; a first build needs network access for uncached sources. `PHOSPHOR_OCIO_BRIDGE` is an optional explicit deployment override. No GPU playback JSON file is needed. Tagged Rec.709, sRGB, linear Rec.709/2020, PQ and HLG inputs convert on the GPU to the linear Rec.709 working image, without a display view or tone map. PQ is normalized to 100 nits per working-space unit; HLG uses the upstream Rec.2100-HLG 1000-nit display definition. Extended HDR values survive composition. The source config is OCIO's built-in `ocio://studio-config-v5.0.0_aces-v2.1_ocio-v2.6`. Source interpretation and receiver delivery use upstream transforms rather than local transfer curves or gamut matrices. Untagged eight-bit video uses the documented SDR Rec.709 interpretation; missing range/chroma siting use narrow range/left siting and are reported in the logs. Untagged high-bit-depth video is rejected instead of guessing its HDR transfer. `PHOSPHOR_GPU_FILE_CONFIG` optionally overrides source transforms and tuning. Decoder session and workload quotas are optional; absent quotas let native NVDEC capabilities and resource availability determine admission. The default ingress memory ceiling follows the selected GPU's physical capacity; allocations remain charged against the renderer's shared native memory budget. For example, this explicit deployment profile accepts sRGB-tagged Rec.709-primary video (CICP primaries 1, transfer 13): The decode costs below illustrate the policy schema; use workload-qualified costs for the selected GPU rather than treating the example as a benchmark. ```json { "max_active_decoders": 2, "decode_rates": { "capacity": 100, "workloads": [{ "codec": "h264", "profile": "High", "pixel_format": "yuv420p", "max_width": 3840, "max_height": 2160, "max_frame_rate": {"numerator": 60, "denominator": 1}, "cost": 50 }] }, "bridge": "/opt/phosphor/lib/libphosphor_ocio.so", "ocio_config": "/opt/phosphor/color/config.ocio", "working_space": {"kind": "named", "name": "lin_rec709_scene"}, "profiles": [ {"primaries": 1, "transfer": 13, "source_space": "sRGB Encoded Rec.709 (sRGB)"} ], "still_profiles": [ {"source": {"kind": "srgb"}, "source_space": "sRGB Encoded Rec.709 (sRGB)", "source_alpha": "straight"}, {"source": {"kind": "untagged_srgb"}, "source_space": "sRGB Encoded Rec.709 (sRGB)", "source_alpha": "straight"} ], "upload_slots": 3, "max_upload_bytes": 134217728, "lut_upload_bytes": 67108864, "lut_resident_bytes": 134217728, "backpressure_timeout_ms": 2000 } ``` Space names must exist in that OCIO config; `working_space` must represent the renderer's scene-linear Rec.709 working contract. Declare additional CICP primaries/transfer pairs with their intended OCIO source spaces, including HDR. BT.709 transfer code 1 requires its own interpretation; it is distinct from sRGB and Rec.1886. Missing or duplicate profiles, unsupported codecs, incompatible GPUs and exhausted capacity fail explicitly. There is no CPU video fallback. `still_profiles` selects the highest-priority embedded declaration, with separate policies for tagged and untagged files. Source alpha must be explicitly declared. Premultiplied RGB is unassociated in full precision before OCIO, with zero RGB for zero input coverage; source coverage is preserved in the working image. Configured still playback never falls back to the legacy raster adapter when a profile is missing. EXR/HDR float samples remain full precision through the input transform; invalid or unrepresentable working pixels fail GPU validation before output publication. ICC selectors bind exact profile bytes by BLAKE3 to an installed OCIO source transform. Optional `"automatic_icc": {"mode": "ocio_matrix_trc", "source_alpha": "straight"}` instead compiles embedded RGB matrix/TRC ICC v2/v4 profiles through OCIO. Exact configured selectors take precedence. Unsupported profiles, including CLUT profiles, fail explicitly; there is no sRGB fallback. Conversion preserves out-of-gamut working values and runs on the GPU, without CPU pixel conversion. This path uses the profile's colorimetry with OCIO's D50/D65 adaptation and linear Rec.709 conversion; it does not implement perceptual gamut mapping or black-point compensation. `max_active_decoders` is required and shared by all configured factories targeting the same physical GPU within this process, keyed by its renderer device UUID. It counts native decoders, including paused sources and exported frames still retiring after source removal. Admission happens before native initialization; the final native device/frame owner returns the permit. Choose this session ceiling from workload qualification: it is not a universal frame-rate guarantee or a reservation against other processes using the GPU. `decode_rates` is also required. Each source reserves the full cost of a matching codec/profile/coded-pixel-format envelope, not a guessed cost scaled from its resolution or average frame rate. Matching envelopes share one capacity domain; the lowest configured cost wins, preferring a higher rate ceiling on cost ties. Declare separate qualified costs for smaller resolutions, other codecs and HDR pixel formats. Unknown rate metadata or unmatched workloads fail before native decoder creation. Decoded timestamps and dimensions are checked continuously; cadence uses exact stream time-base arithmetic with one tick of rounding tolerance. Idle timestamps cannot bank burst credit, and a contract violation remains fatal for that decoder, including subsequent seek/prefetch calls. Both session and rate reservations remain charged through final native frame retirement, including paused sources. Native reservations retain their GPU's admission domain after factories or prepared configurations are dropped; another factory cannot reset that allowance. Factories on that GPU must declare the same session limit and workload policy. Row ordering and equivalent rational rates are canonicalized; incompatible policies are rejected until all domain owners retire. Different GPU UUIDs have independent domains. Source workers reject a domain that does not match the selected renderer GPU before CUDA lookup or file open. Run native decoder qualification tests serially because independent test fixtures share these limits. These administrative budgets do not establish a hardware throughput guarantee, seek-latency guarantee or cross-process reservation. `max_upload_bytes` also bounds aggregate renderer-owned ingress allocations across all file nodes from that factory. Native allocation alignment counts against it, and retained upload/import owners remain charged across pool replacement. Increase this explicit ceiling for more simultaneous sources; exhaustion returns an error rather than allocating outside it. This does not yet account for FFmpeg/NVDEC's internal codec surfaces. Input LUT images have a separate shared `lut_resident_bytes` ceiling, defaulting to `max_upload_bytes` when omitted. The upload window remains independently bounded by `lut_upload_bytes`; a resident cache hit shares one allocation charge. The local OCIO installation/profile configuration is immutable for a factory's lifetime; restart to apply installation changes. Rship asset and playback controls continue to update dynamically. Standard Rec.709 delivery also uses the upstream OCIO Camera Rec.709 processor, including linear-light background composition, rather than a separate handwritten encoding shader. The Cargo build supplies the native bridge automatically. For receiver-bound OCIO transforms, use the Rivermax GPU backend. An optional `PHOSPHOR_OCIO_BRIDGE` override must name a trusted deployment library. The executable validates the bridge at startup; configs, manifests and LUTs come from the receiver binding's pinned rship asset revisions. Bundles must resolve `lin_rec709_scene`, matching the current renderer working space. Missing aliases, assets, unsupported signal formats and preparation errors are reported explicitly. Receiver processing uses the completed float working image, composites its background in linear light, applies the selected view/color-space processor, and packs the GPU-resident result without a second transfer curve. OCIO delivery requires transform-defined encoding with explicit final-transfer metadata. Current wire support is opaque narrow-range YCbCr 4:2:2 at 8/10 bits, with 10-bit packing for wide-gamut/HDR signals. Float delivery currently requires one explicitly selected GPU; multi-GPU working-image placement remains unfinished. Asset preparation and bounded native preparation happen outside frame admission and ring installation. Equivalent prepared receiver transforms for one composed Output share a GPU float producer keyed by its working-image generation and exact frame revision. Each binding keeps its own packing format, payload credits, deadline and cancellation state. A stalled or removed receiver cannot pin the shared float image through its NIC payload lease. Sharing currently requires matching prepared shader identity and admission domains; it does not merge independently compiled programs merely because they may be numerically equivalent. Single-GPU RGBA8 preview conversion runs only while a hardware preview pool is retained. Pool demand belongs to the exact target generation; allocation pins alone do not request conversion. CPU snapshots read the actual completed working image. A new GPU preview waits for a converted revision, preserving the exact source image rather than publishing untouched preview bytes. Existing independent GPU tile gathering remains an explicit internal preview-buffer consumer. Preview buffer allocation is still eager; demand-driven allocation remains unfinished. Vulkan allocations share per-heap ceilings and a physical-GPU VRAM aggregate across logical devices in this process. Renderer CUDA payload rings also consume that aggregate, identified by the exact CUDA/Vulkan GPU UUID. Multiple local Vulkan heaps remain valid; CUDA memory is not guessed into a particular heap. Working images, previews, uploads, transforms, LUTs and readbacks remain charged through native destruction. CUDA payload charges survive producer replacement and release only after their queued free completes. Aggregate VRAM includes its per-heap and payload sub-totals; do not add those totals together again. Capacities are physical ceilings, not free memory or reserved GPU shares. Vulkan logical groups conservatively reserve all members; independent-device backends retain separate GPU budgets. Decoder surfaces, driver-internal memory and other processes remain outside this accounting. The server delivery factory shares separate float-output, packed-payload and resident receiver-LUT ceilings across profiles and output generations. LUT cache hits in the same admission domain share one resident allocation and charge; other domains cannot reuse it without accounting. LUT upload-window limits stay separate from resident-image capacity. Vulkan memory requirements and CUDA DMA-BUF-rounded payload allocations are charged; retained frames keep their allocations charged after ring replacement. Payload charges release only after stream-ordered CUDA deallocation completes, without blocking the frame worker. This path requires CUDA stream-ordered deallocation. Each ceiling currently uses the configured transport buffering byte limit. Decoder codec surfaces, composition-image, BAR1 and NIC bandwidth aggregate accounting remain unfinished. Changing `positionMs` seeks. Increment `seekRevision` to seek again to the same position. When changing both, publish them in one rship update to avoid two transient seeks. The authored `loop` anchor controls `FilePlayback::looping`. `playing=false` holds the current frame; resuming preserves that playhead. Identical desired snapshots preserve active players and retry failed players. Non-looping playback holds the final frame; looping wraps at the video duration. Removing the final output or preview consumer retires the player. A newly demanded player starts at the declared position. Separate file nodes have independent playheads even when they select the same cached asset. Output presentation epochs select file media positions before render admission. Each file has one hardware-decoder worker, with eight pending work slots (including a coalesced wakeup) and at most `upload_slots - 2` worker-cache entries. The remaining slots cover the last published preview frame and the next upload. The default uses four slots. In-flight readers separately pin their exact slots; the fixed GPU pools enforce capacity. Outputs sharing a decoded media interval reuse its immutable prepared sample. All cache-miss decoder requests queue before any wait. Saturation and missed deadlines fail explicitly; the scheduler does not substitute the latest arrival. A completed late decode may remain cached for a later valid request, but cannot publish as the missed output frame. Outputs select completed GPU images from the bounded media-interval cache, including prepared futures, without joining a busy decoder's queue. Selection uses the frozen physical epoch/media relation and current playback version. An interval miss still requires decoder work; no latest-image substitution is made. Pause/seek/stop, expired deadlines and worker failure invalidate tickets. Worker exit releases the cache before native decoder teardown completes. After a successful playing selection, the worker fills contiguous future intervals through GPU upload and OCIO binding preparation up to its configured cache capacity, then fills the bounded two-frame raw decode-ahead queue. Refill retains the selected interval, replaces history, and checks queued work between preparations. Completed futures stay readable even while subsequent native mapping blocks. Preparing them does not publish or display them: publication happens only on selection, and an older physical query cannot rewind the latest source observation. Already queued work takes priority. Coalesced consumer progress refills the prepared queue without a request per output or idle polling; the progress slot survives a full work queue. Stop/failure explicitly wake idle workers. Renderer publication uses weak references to avoid a resource cycle. Codec frame threading remains independently bounded. Cached futures contain NV12/P010 surfaces; source color conversion executes directly into the working stage image when the renderer materializes that stage. Copied input samples retain metadata and destination read leases, rather than pinning decoder surfaces through composition. Completed copy events release source surfaces even when downstream readers still hold the destination image. Each file has an explicit nonblocking CUDA producer stream, retained by FFmpeg's device/frame references, instead of sharing the CUDA legacy default stream. FFmpeg/NVDEC's opaque internal allocations remain outside the VRAM ledger. Play/pause/seek intent is timestamped on receipt. The first request for each intent version establishes its media/PTP relation; subsequent progress uses physical PTP presentation differences without resampling local clock jitter. Identical control snapshots do not reanchor playback. This does not establish synchronized file playheads across hosts: rship intent still lacks a shared authored start epoch. With no epoch demand the decoder stays idle; diagnostics using `render_next` sample the last published source rather than advancing an independent playback clock. See the [playback design](docs/specs/2026-09-10-asset-file-playback.md) for ownership and validation details. The dependency baseline is the published rship `4.0.0-canary.141` SDK, including its shared external-node contracts. These dependencies do not require a local rship worktree or Cargo patches. Run the hardware decoder-to-Vulkan control and pixel checks with the installed bridge and an OCIO config containing the example spaces: ```sh PHOSPHOR_OCIO_BRIDGE=/path/to/libphosphor_ocio.so \ PHOSPHOR_OCIO_TEST_CONFIG=/path/to/config.ocio \ scripts/verify-file-playback.sh ``` Its multicast check uses the diagnostic UDP loopback adapter; it does not qualify production NIC DMA or wire timing. With the same OCIO environment, the ignored library test `measure_four_4k_sources_on_one_shared_renderer` measures four independent GPU0 decoders, one renderer and eight 8/10-bit output rings. It reports complete foreground-plus-decode-ahead cycles and counts cycles exceeding 1/60 second. It excludes setup, decoder retirement, diagnostic pixel reads and NIC work. This is a short diagnostic measurement, not a sustained 4K60 capacity gate. Current evidence includes long NVDEC mapping waits; four independent 4K60 decodes on this GPU have not passed throughput qualification. The separate ignored `measure_prepared_4k_source_delivery_at_60hz` and `measure_two_prepared_4k_sources_delivery_at_60hz` tests use the real file actors, configured source-color factory, exact epoch inputs, composition and GPU packing in short paced windows. They check advancing source PTS and GPU-ready deadlines. They use synthetic PTP and no NIC, so they do not qualify production wire timing. The script generates a deterministic video with FFmpeg, then verifies pause, resume, forward/backward/repeated seek, loop, shared demand, and retirement against a Vulkan 1.3 device. Asset transfer has a separate local Iroh SDK test: ```sh cargo test -p phosphor-rship assets:: ``` ### Production-path gaps These foundations are not a production-ready clustered renderer. Remaining work includes: - validating peer access, work partitioning, barriers, owner export, sustained deadline behavior, and failure isolation on the intended multi-A4000 Linux guest; - validating the linuxptp/PHC provider against the deployment's real ptp4l, PHC, NIC, kernel, and grandmaster; - making logical `ViewTileAssignment` drive Vulkan allocation, materialization, residency updates, and view assembly; - validating the configured service supervisor restart policy and coherent graph/source replay after an injected GPU loss or partial-device failure. Cross-node transfer and GPUDirect RDMA remain represented by the architecture, but are deliberately outside the current single-host production milestone. ### Service supervision Production must run Phosphor under a supervisor that restarts the whole process after a terminal GPU-set quarantine. Install `packaging/systemd/phosphor.service` as `/etc/systemd/system/phosphor.service`, copy `packaging/systemd/phosphor.env.example` to `/etc/phosphor/phosphor.env`, replace every deployment-specific value, and install the release binary as `/usr/local/bin/phosphor`. The unit sends SIGINT for an orderly bounded shutdown, restarts only non-successful exits, and limits restart storms to three attempts per 30 seconds. A production rollout is not qualified until an injected device loss proves that systemd restarts the process and rship replays the complete graph/source state. ```sh sudo install -D -m 0644 packaging/systemd/phosphor.service \ /etc/systemd/system/phosphor.service sudo install -D -m 0600 packaging/systemd/phosphor.env.example \ /etc/phosphor/phosphor.env sudo systemctl daemon-reload sudo systemctl enable --now phosphor.service ``` ### Production hardware qualification Configure the NIC and system clocks using [Linux clock setup](docs/linux-clock-setup.md). For passed-through GPUs, also follow [Proxmox GPU peer access](docs/proxmox-gpu-peer-access.md). Two ignored tests form the single-host deployment gate. The Vulkan smoke test first requires exact independently enumerated GPU UUIDs, changes a black sentinel input to magenta, and verifies the CUDA-gathered delivery buffer, the band-assembled BT.709 program frame, and the band-assembled sRGB native preview. It fails unless every expected GPU is a distinct local backend. The `production_lockstep` test then uses the same local coordinator with the production cached linuxptp provider and sustains 59.94 PTP-aligned renders for at least 120 frames. The default run is 600 frames at 1920x1080. It first qualifies the explicit scheduler boundary, then drives another full run through the production `OutputManager`. The managed run must preserve consecutive ST 2059 epochs and coordinated revisions, consult authoritative PTP health before and after rendering, remain above the configured cadence floor, and avoid GPU quarantine. Exact program revisions and the final managed frame are assembled and checked for the expected pixels: ```sh export ST2110_TEST_PTP4LRO=/run/ptp4lro export ST2110_TEST_PTP_INTERFACE=enp1s0f0 export ST2110_TEST_PTP_UID=0 export ST2110_TEST_PTP_DOMAIN=0 export ST2110_TEST_PTP_TRANSPORT_SPECIFIC=0 export ST2110_TEST_PHC_INDEX=0 export PHOSPHOR_TEST_PTP_MAX_OFFSET_MICROSECONDS=1000 export PHOSPHOR_TEST_EXPECTED_VULKAN_UUIDS=0123456789abcdef0123456789abcdef,fedcba9876543210fedcba9876543210 # Optional overrides; the minimum accepted frame count is 120. export PHOSPHOR_TEST_QUALIFICATION_FRAMES=600 export PHOSPHOR_TEST_QUALIFICATION_WIDTH=1920 export PHOSPHOR_TEST_QUALIFICATION_HEIGHT=1080 export PHOSPHOR_TEST_MINIMUM_MANAGED_FPS=55 export PHOSPHOR_TEST_MANAGED_TIMEOUT_SECONDS=30 cargo test --release -p phosphor-vulkan --test vulkan_smoke \ gathers_independent_device_bands_into_complete_delivery_and_program_frames \ -- --ignored --exact --nocapture cargo test --release -p phosphor-server --test production_lockstep \ qualifies_ptp_scheduled_multi_gpu_render_on_production_hardware \ -- --ignored --exact --nocapture ``` The tests must run where the intended GPUs are exposed to one Linux guest as independent singleton Vulkan groups and CUDA reports direct peer access between the deterministic owner and every peer. The PTP test additionally requires a ptp4l read-only management socket, NIC hardware timestamping, and PHC. Both tests intentionally fail instead of skipping when an exact device identity, backend partition, peer path, PTP identity, role, grandmaster, offset, freshness, sustained deadline, revision, or pixel requirement is missing. ## Build and run Phosphor pins its Rship SDK and entities to the published `4.0.0-canary.130` release. A local Rship checkout is not required, and Bacon watches only Phosphor and its active transport dependencies. Keep the complete Rship dependency graph on the same release when updating it. ```sh cargo build -p phosphor-server cargo test --workspace cargo clippy --workspace --all-targets -- -D warnings cargo run -p phosphor-server # In a second terminal: cargo run -p phosphor-viewer ``` Running requires a Vulkan loader and a Vulkan 1.3-capable device exposing a graphics+compute queue. It also needs rship at `RSHIP_URL`, which defaults to `ws://localhost:5155/myko`. Phosphor also serves Myko at `PHOSPHOR_MYKO_ADDR` (default `0.0.0.0:5165`) with `NullPersister`. The raw rship client publishes only the WHEP `Stream` declarations Phosphor owns; it does not persist copies of rship entities. `PHOSPHOR_VULKAN_MIN_RENDER_PARTICIPANTS` defaults to `1`. Independent workers require all three settings: the exact comma-separated UUID set in `PHOSPHOR_VULKAN_EXPECTED_DEVICE_UUIDS`, its count in `PHOSPHOR_VULKAN_MIN_RENDER_PARTICIPANTS`, and explicit component anchors in `PHOSPHOR_WORKER_PLACEMENT` JSON. For example, replacing stage IDs and UUIDs with actual graph identities and available devices: ```json {"stages":[{"id":"output-a","device_uuid":"01010101-0101-0101-0101-010101010101"},{"id":"output-b","device_uuid":"02020202-0202-0202-0202-020202020202"}],"sources":[]} ``` In this mode the participant count specifies independent workers; each graph component renders on its assigned singleton worker. UUIDs must be available and unique stage/source anchor IDs must be nonblank. Dormant anchors are permitted. Source anchors use the same entry shape in `sources`. All connected stages and shared inputs must resolve to one owner. A component without an anchor rejects. Without placement configuration, the service requires one qualified worker. Multi-GPU desktop Output windows are not supported: set `PHOSPHOR_OUTPUT_WINDOWS=false`. No GPU is selected as an implicit desktop owner. Decode/upload/LUT and output preparation budgets are applied independently per selected device; these configured caps are not a measured throughput guarantee. For Rivermax GPU output, optional `PHOSPHOR_MEDIA_LINKS` JSON assigns each Output to an explicit local source link. It uses the normal ST 2110 sender settings as a template and overrides source IPv4 and wire-rate quota per link: ```json {"links":[{"id":"media-a","source_ipv4":"192.0.2.10","budget_bps":90000000000}],"outputs":[{"output_id":"output-a","link":"media-a"}]} ``` Replace the illustrative address and Output ID with actual configuration. Outputs assigned to the same link share its quota; different links have independent quotas. Reservations remain charged until native flow retirement. Receiver destinations and per-binding delivery profiles still come from rship. All receiver bindings of an Output use that Output's selected link. An unassigned Output can render without transport receivers; an ST 2110 binding requires an explicit assignment. Duplicate link IDs/source addresses and output assignments reject. Startup also requires each source address to have one local interface owner and rejects separate link quotas for aliases on the same interface. Link policy is configured once before output reconciliation; hot link migration is not implemented. Physical source-to-port mapping, aggregate capacity and multi-NIC timing/throughput remain unqualified. Other sender backends reject this configuration instead of substituting an interface. Every scheduled Output is admitted by one process-level PTP discipline gate, whether or not it has an ST 2110 route. A multi-GPU configuration automatically requires verified linuxptp/PHC health and rejects `PHOSPHOR_PTP_ALLOW_UNVERIFIED_TAI=true`. This makes PTP epoch admission a renderer invariant rather than a side effect of packet transport. On Wayland or X11, the server opens one raw completed-frame window for every active Output and reconciles those windows as Outputs appear or disappear. Set `PHOSPHOR_OUTPUT_WINDOWS=false` for headless operation. Raw ST 2110 output is activated only when a feed has an ST 2110 receiver route. Configure the Output's rship `Receiver::St2110` with the destination multicast group, UDP port, RTP payload type, SSRC, MAXUDP, multicast TTL, and DSCP. Every receiver route gets its own sender and receives the completed Output canvas. The current wire format is progressive BT.709 narrow-range 10-bit YCbCr 4:2:2 with 720 or 1080 active lines. Packets use zero-based sample rows and a fixed-size single-row layout suitable for Rivermax receivers. See [Rivermax receiver setup and qualification](docs/rivermax-compatibility.md) for packet sizes, Unreal/DeepStream settings, and the hardware receiver test. For example, a receiver route can use group `239.100.0.1`, port `5004`, payload type `96`, SSRC `42`, MAXUDP `1400`, TTL `1`, and DSCP `34`. On the sender, select the local address of the NIC connected to the multicast network: ```sh PHOSPHOR_ST2110_BIND=0.0.0.0:0 \ PHOSPHOR_ST2110_INTERFACE=192.168.100.10 \ PHOSPHOR_ST2110_TXTIME_POLICY=disabled \ PHOSPHOR_PTP_ALLOW_UNVERIFIED_TAI=true \ cargo run -p phosphor-server ``` Replace `192.168.100.10` with an address assigned to your sender. The interface setting is a local IPv4 address, not an interface name or the destination group. Receivers join the configured group on their receiving interface. This example uses single-GPU development clock admission and immediate UDP delivery; it does not establish verified PTP or ST 2110-21 wire pacing. Use the verified PTP settings below for production clock admission. Run `scripts/verify-file-playback.sh` to check playback controls and receive file-derived Vulkan pixels through a multicast group on loopback. The test reassembles a complete RFC 4175 frame and checks its pixel values, payload type, and SSRC. It requires FFmpeg and a Vulkan 1.3 device. The host-local sender settings are explicit: - `PHOSPHOR_ST2110_BIND` — local UDP bind address; omit it for preview-only use. - `PHOSPHOR_ST2110_INTERFACE` — optional IPv4 multicast egress address. - `PHOSPHOR_ST2110_LINK_BUDGET_BPS` — optional positive bandwidth quota for this process's media interface. Omit it to impose no configured quota. If desired, provision it from the VM's reserved network share; the interface's reported speed is not a reservation. All active output streams share this quota across sender backends. Mirrored receivers share one stream charge; distinct streams are charged separately. Admission includes exact rational cadence, packet headers and conservative single-VLAN Ethernet framing. Reservations remain charged until native senders retire. Exhaustion retries admission without stopping composition. This bounds average framed bandwidth; it does not qualify burst shaping, physical wire timing or other processes. - `PHOSPHOR_ST2110_TXTIME_POLICY` — `disabled`, `prefer`, or `require`. Verified production output requires `require`. The other policies are development-only and require unverified TAI admission. - `PHOSPHOR_ST2110_TXTIME_LEAD_MICROSECONDS` — nonzero enqueue lead for TXTIME; it must be less than one exact frame period because Phosphor prepares one frame early. - `PHOSPHOR_PTP_ALLOW_UNVERIFIED_TAI=true` — explicit single-GPU development mode using `CLOCK_TAI` without claiming verified PTP health. Multi-GPU configuration rejects it. - `PHOSPHOR_PTP_MAX_OFFSET_MICROSECONDS` — maximum absolute measured PTP offset accepted in verified mode. - `PHOSPHOR_PTP_MAX_AGE_MILLISECONDS` — maximum age of the provider's health observation at epoch admission. - `PHOSPHOR_PTP_ALLOW_HOLDOVER` — whether verified mode may accept an otherwise healthy observation marked as holdover. - `PHOSPHOR_PTP_ACCEPTABLE_ROLES` — comma-separated subset of `slave`, `master`, and `passive` accepted for verified operation. The four PTP policy variables are required when unverified TAI is disabled. Development mode may omit them; its fallback values do not fabricate health or allow the default provider to report `Verified`. For the shared `linux_ptp_clock` role in pulse-deploy, select `PHOSPHOR_PTP_HEALTH_MODE=software_system` and `PHOSPHOR_PTP_UDS_READ_ONLY_ADDRESS=/run/ptp4lro`. This reads the standard daemon without an application clock writer, custom declaration or daemon patch. See [the shared-daemon integration](docs/linux-clock-setup.md). Verified Linux operation also requires the following probe settings: - `PHOSPHOR_PTP_UDS_READ_ONLY_ADDRESS` — linuxptp's read-only management socket, normally the configured `ptp4lro` endpoint; - `PHOSPHOR_PTP_INTERFACE` — exact ptp4l port interface name; - `PHOSPHOR_PTP_EXPECTED_SERVER_UID` — trusted owner/sender UID for the management socket and authenticated response datagrams; - `PHOSPHOR_PTP_DOMAIN_NUMBER` and `PHOSPHOR_PTP_TRANSPORT_SPECIFIC` — values correlated on every management response; - `PHOSPHOR_PTP_EXPECTED_PHC_INDEX` — hardware modes only: exact `/dev/ptpN` associated with the selected port; - `PHOSPHOR_PTP_RESPONSE_TIMEOUT_MILLISECONDS` and `PHOSPHOR_PTP_SAMPLE_INTERVAL_MILLISECONDS` — both must remain below the admission policy's maximum age, and three response budgets plus the sample interval must fit strictly inside that age; - `PHOSPHOR_PTP_MAXIMUM_INGRESS_AGE_MILLISECONDS` — maximum accepted age of ptp4l's last-Sync evidence; - `PHOSPHOR_PTP_CROSS_TIMESTAMP_SAMPLES` — hardware modes only: `1..=25`; and - `PHOSPHOR_PTP_MAXIMUM_CROSS_TIMESTAMP_SPAN_MICROSECONDS` — hardware modes only: maximum accepted uncertainty window for the selected PHC/TAI sample. Hardware modes' PHC-to-`CLOCK_TAI` extended cross timestamp requires a Linux kernel with the CLOCK_TAI-capable PTP UAPI (Linux 6.12 or newer). Older kernels fail verified startup/sampling honestly; they do not fall back unless the operator explicitly selects unverified development mode. The former `PHOSPHOR_ST2110_PTP_*` names and `PHOSPHOR_ST2110_ALLOW_UNVERIFIED_TAI` remain accepted as compatibility aliases; new deployments should use the transport-independent names above. `prefer` falls back once for the whole socket when `SO_TXTIME` setup fails. `require` fails that route instead. Either failure leaves the render cadence and preview stream active. Linux TXTIME telemetry counts requested packets and asynchronous missed/invalid reports, but compliance remains `ReferenceTimingOnly` and `wire_pacing` remains unclaimed. A matching ETF qdisc and measured NIC/wire behavior are deployment concerns outside this process. The operator-facing viewer is a separate native GPUI process. It discovers active render plans, automatically retires removed plans, lets the operator switch outputs, and displays decoded frames with connection and format status: ```sh cargo run -p phosphor-viewer ``` It gets the live render-plan list, raw RGBA frames, and Phosphor's actual rship connection state from the native-viewer transport at `http://127.0.0.1:8088`. Set `PHOSPHOR_PREVIEW_URL=http://render-host:8088` when the viewer and renderer run on different machines. The server listener defaults to `PHOSPHOR_PREVIEW_ADDR=0.0.0.0:8088`. This transport is unauthenticated and should not be exposed to the public internet. The UI does not depend on Vulkan internals and can move to WebRTC or a local zero-copy transport without changing render ownership. Browser previews use the same listener. Phosphor offers every preview plan and starts one in-process low-latency NVENC H.264 session only while that plan has viewers; all viewers of the same plan share it. Single-backend stages allocate exportable preview buffers only when a GPU preview pool is requested; unobserved stages keep only their working images. Preview storage remains reusable after the last viewer leaves, subject to target caching and outstanding export leases. Multi-backend band gathering currently requires preview buffers even without viewers. Rendered preview pixels remain in exportable Vulkan GPU buffers through CUDA/NVENC, while only the compressed H.264 access units return to CPU memory for WebRTC packetization. WHEP signaling is served at `/whep?plan=` and the absolute URL is published as an rship `Stream`; no FFmpeg process or separate media server is involved. This path requires an NVIDIA GPU/driver with NVENC and Vulkan external-memory support, but no CUDA toolkit is linked at build time. Optional overrides: Screen and Screen-Mapping-Usage previews are aspect-preserving GPU views bounded to 1280x720 by default. Mapping previews observe the exact completed native mapping image consumed by Outputs; they do not rebuild its graph at preview resolution. Screen previews observe the exact native Screen image sampled by Outputs. These observation edges do not advance their producers, decoders or procedural clocks. Native Screen definitions are shared by compatible Outputs and retired only after their last owner releases them. An orphan Screen preview may drive its own native stage instead. When one Screen/session has distinct render contexts, Phosphor publishes its broad preview as unavailable with a reason, while keeping Output previews usable. The current rship widget still needs context selection and unavailable-reason presentation. Native Screen storage currently requires a real GPU-sized image; bounding its preview does not reduce that producer allocation. Sparse tiling, identity-image aliases and lazy orphan-stage allocation remain unfinished. - `PHOSPHOR_STREAM_PREVIEWS=false` disables browser preview publication. - `PHOSPHOR_WHEP_PUBLIC_BASE=http://render-host:8088` sets the browser-facing base when automatic local-address discovery is unsuitable. - `PHOSPHOR_WHEP_ICE_BIND=0.0.0.0:0` changes the WebRTC UDP bind; it defaults to all interfaces and an ephemeral port. - `PHOSPHOR_WHEP_MAX_VIEW_WIDTH=1280` and `PHOSPHOR_WHEP_MAX_VIEW_HEIGHT=720` bound Screen and Mapping-Usage preview rasters without changing logical canvas dimensions. To exercise the real Vulkan-to-native-viewer path without rship: ```sh cargo test -p phosphor-server --test preview_smoke -- --ignored ``` Run the watch loops from the repository root in separate terminals: ```sh bacon server # default job; debug server with live logs bacon viewer # release viewer bacon server-release # optional release server ``` The jobs in [`bacon.toml`](bacon.toml) restart on source, shader, `.env`, Cargo manifest, lockfile, and Cargo configuration changes. They watch the relevant Phosphor crates and local siblings in a Pulse workspace. Server and viewer jobs use separate build caches under `target/bacon-*`, so they can rebuild concurrently. Both server jobs set `PHOSPHOR_OUTPUT_WINDOWS=false`; use `bacon viewer` to monitor outputs. The server also tees logs to `phosphor-server.log`, truncating on each restart; log writes do not trigger restarts. `RUST_LOG` overrides the default filter. These jobs use Linux `setsid` and Bash to stop the whole process group before restarting. In Bacon, `r` selects the server, `Shift-R` the release server, `v` the viewer, and `Ctrl-R` toggles raw output. See [Bacon's configuration reference](https://dystroy.org/bacon/config/) for job and watch settings. ## Task tracking Phosphor uses Levi: ```sh levi next --claim --json levi ls --json levi show --json ``` --- slug: /tactus/readme title: tactus source: tactus --- # Tactus See [ADSR control analysis](docs/adsr.md) for the segmented native envelope model, its output schema, and the WAV-to-JSON example. Tactus analyzes music, separates stems, and publishes analysis to Rocketship. The browser compares results on a shared timeline and controls local or server-owned audio playback. The engine stores entity state in Myko and audio and analysis artifacts on disk. The engine, DSP library, and UI are Rust. External analysis services and the MSST stem separator use Python. See [Generated engine contracts](docs/design/engine-contracts.md) for the Python-to-Rust boundary model and [Regenerate engine contracts](docs/development/engine-contracts.md) for generation commands. ## Repository map | Path | Responsibility | | --- | --- | | `tactus-types/` | Shared entities, commands, reports, analysis routes, parameters, and document contracts | | `tactus-dsp/` | Native beat, tempo, onset, structural segmentation, chroma, key, and chord analysis, plus decoding, click rendering, and waveform peaks | | `tactus-engine/` | Myko server, HTTP API, run execution, external backend clients, source storage, playback, and rship publication | | `tools/engine-contract-*/` | Python-schema type generation and Rust semantic adaptation macros | | `apps/analysis-*/` | All-In-One, SongFormer, SuperFlux, Frame-RNN (ADTOF), ADT_STR, YourMT3+, Transkun, and Libsonare HTTP services | | `apps/msst/` | MSST-WebUI fork, tracked as a Git submodule | | `apps/ui/ui-frontend/` | Leptos WASM frontend, built with Trunk | | `apps/ui/ui-server/` | Static UI server with an embedded bundle or filesystem dist directory | The root Cargo workspace contains `pulse-tactus`, `tactus-types`, `tactus-dsp`, `tactus-engine`, and the engine contract tools. The frontend builds separately. The UI server belongs to the separate workspace at `apps/ui/`. ## Local services [Docker Compose](docker-compose.yml) defines the following services. Only the engine, UI, and MSST ports are published to the host. | Service | Container port | Role | | --- | --- | --- | | `tactus-engine` | 5001 | Myko, MCP, HTTP, analysis orchestration, and playback | | `ui` | 3000 | Browser UI | | `tactus-pg` | 5432 | Postgres event store | | `msst-webui` | 7860 | GPU stem separation | | `all-in-one` | 8000 | Semantic structure, rhythm grid, activations, and embeddings | | `songformer` | 8000 | Semantic structure | | `superflux` | 8000 | Onset detection | | `transkun` | 8000 | Piano note transcription with predicted velocity; CPU by default | | `libsonare` | 8000 | Music analysis and acoustic profiles | | `adtof` | 8000 | Frame-RNN drum transcription trained on ADTOF; `noncommercial-analysis` profile | | `adt-str`, `yourmt3-plus` | 8000 | Optional drum transcription models; `drum-comparison` profile | For the default Compose stack, configure NVIDIA container support first. Compose requests GPUs for MSST, All-In-One, and SongFormer. From the repository root, initialize MSST and install the seed audio and stem models before building: ```sh git submodule update --init --recursive apps/msst ./setup.sh docker compose up --build ``` Open `http://localhost:3000`. `./setup.sh --check` reports seed audio and model availability without downloading. Setup defaults to `./data` and `apps/msst/pretrain`; `TACTUS_DATA_DIR` and `TACTUS_MSST_MODELS_DIR` override those setup paths. Compose uses its own explicit bind mounts, so changing a setup variable does not change the container mounts. Frame-RNN (ADTOF) is optional. Compose mounts `./data/models/adtof` over `/models`, so that host directory must contain `adtof.pth` before enabling the profile. The mount hides the weights bundled in the image. Backend configuration and the supported analyses are documented in [Analysis backends](docs/analysis-backends.md). For the newer drum models, velocity output, and repeatable comparisons, see [Drum backend comparison](docs/drum-backends.md). For a native engine using only the Rust DSP, run: ```sh TACTUS_DATA_DIR="$PWD/data" cargo run --locked -p tactus-engine ``` This command uses in-memory entity state unless `TACTUS_POSTGRES_URL` is set. It does not start external services or the UI. Compose service hostnames do not resolve automatically from a native process; set backend URLs to reachable addresses when using external analyses. ## State and execution The UI submits typed `StartOperation` commands. An analysis invocation contains an `AnalysisRoute`, which pairs an analysis kind with a backend, and matching parameters. The legacy string-based `StartRun` command remains supported. The run reconciler scans every 500 ms, dispatches queued work, records progress as `RunEvent` entities, and marks runs completed or failed. Native DSP executes in blocking tasks. External analyses use HTTP, and stem separation uses MSST. On startup, interrupted running jobs are marked failed. Queued jobs remain eligible for dispatch. An `AnalysisRecord` identifies a run's result, parameters, producer, and available outputs. The reconciler preserves a separate document for each run and queues supported click renders from those recorded outputs. See [Result storage](docs/analysis-backends.md#result-storage) for the artifact layout. Uploads enter through `POST /api/upload` using the multipart field `audio_file`. The engine commits a source package and immutable blob before exposing the playable source. Startup also recovers committed packages and imports legacy files from the data directory. There is no continuous folder watcher. Postgres retains Myko events. Without `TACTUS_POSTGRES_URL`, the engine uses `BlackholePersister`: entity state is lost at restart, although audio and analysis files remain on disk and migration can recover some records. Filesystem recovery is not a replacement for the event store. Compose configures Postgres with a named volume. ## API and playback Every engine transport shares one listener, default `0.0.0.0:5001`. | Path | Purpose | | --- | --- | | `/myko` | Myko WebSocket commands, live queries, and reports | | `/myko/mcp` | MCP over HTTP | | `GET /health` | Engine liveness | | `POST /api/upload` | Audio upload, with an optional UUID `Idempotency-Key` | | `GET`, `HEAD /api/blobs/{blob_id}` | Immutable audio bytes with range support | | `GET /api/{inputs,stems,click-tracks}/{filename}` | Audio by filename, with range support | | `GET /api/{inputs,stems,click-tracks}/{filename}/peaks` | Waveform peak mipmap | | `GET /api/analysis/{analysis_id}` | Public document for an analysis record | | `GET /api/services/analysis` | External service status probes | | `POST /api/sources/{source_id}/purge` | Permanent deletion of an archived source | | `/api/playback/live-audio` | WebSocket carrying server PCM audio | The server owns the playback clock used for live analysis. Attached browsers listen to its PCM stream. Detached browsers use local Web Audio playback. Selected analysis routes supply live rship emitters. Durable EventTrack publication is an application setting and is off by default. The frontend resolves the engine from `?myko=host:port`, then the compile-time `TACTUS_MYKO_ADDR`, then the page origin. The UI server proxies authentication, APIs, and Myko to the engine. Use the configured-auth dev launcher below for a complete sign-in workflow. Required-auth deployments should use the same-origin proxy; legacy direct-engine overrides do not carry the page session across origins. The UI server accepts `TACTUS_UI_LISTEN`, default `0.0.0.0:3000`, and `TACTUS_UI_DIST` to serve a filesystem bundle instead of the embedded bundle. ## Multi-user presence Signed-in people viewing the same source share participant bubbles and timeline cursors. Cursor positions use media time and follow each viewer's own zoom and scroll. The footer controls let each person stop sharing their cursor or hide other cursors. Multiple tabs for one account share one participant bubble. Configure the identity provider and persistent session storage together: | Variable | Value | | --- | --- | | `TACTUS_AUTH_ISSUER` | OIDC issuer URL | | `TACTUS_AUTH_CLIENT_ID` | Application client ID | | `TACTUS_AUTH_CLIENT_SECRET` | Application client secret | | `TACTUS_AUTH_ORIGIN` | Browser origin, such as `https://tactus.example.com` | | `TACTUS_AUTH_EMAIL_DOMAINS` | Comma-separated allowed verified email domains | | `TACTUS_AUTH_POSTGRES_URL` | Session database URL. Required unless `TACTUS_POSTGRES_URL` supplies it. | | `TACTUS_AUTH_IN_MEMORY` | Explicit `1` for tests or disposable demos only; mutually exclusive with a session database. | The issuer must support Google code exchange and Auth0-compatible passwordless email. Register the application origin followed by `/` as the callback URL. Serve `/auth/user/*` through the UI server to the engine using `TACTUS_UI_ENGINE_URL`. One engine process owns sign-in transactions and room authorization. Tactus pins the shared authentication and presence crates to `v0.5.1`. Verified `User` profiles use the distinct Myko wire entity `PulseUser`, so the configured Myko persistence keeps them independently of browser sessions without colliding with legacy SDK entities. Issued auth sessions persist in PostgreSQL and survive engine restarts. `TACTUS_AUTH_POSTGRES_URL` selects a dedicated session database; otherwise the engine uses `TACTUS_POSTGRES_URL`. Normal startup requires one of these URLs and fails if session storage cannot initialize. Tests and disposable demos can explicitly select `TACTUS_AUTH_IN_MEMORY=1`, which loses sessions on restart. This option cannot be combined with a session database. Authentication is required before the application mounts. The shared Pulse login screen offers Google and email codes. The standard shared `ProfileMenu auth=auth room=room` account button opens presence settings and Sign out; the adjacent bubbles show other source-room participants. Your profile bubble also carries your room presence, without a duplicate self bubble. Engine APIs and Myko connections require a valid session. Complete authentication configuration is required at engine startup. Room access also requires an existing, unarchived source. Run the isolated browser verification with Rust 1.98, Trunk, Node, and Playwright Chromium installed: ```sh npm ci --prefix tests/e2e node tests/e2e/verify-multi-user.mjs ``` The runner builds and starts its own engine, UI server, and loopback OIDC fixture. It uploads generated audio and tests real browser sign-in, durable user identity, source-scoped Myko presence, independent client playback, cursor controls, and repeated reconnects without production credentials or a running deployment. Results and screenshots are written to `target/multi-user-verification/`. To host the built development UI for collaborators, build the engine, UI server, and frontend first. The launcher starts no compilers, binds child services to loopback, uses the chosen data directory, and exposes only the HTTPS front door. Configure the five identity-provider variables above and a session database. Set the origin to the exact public URL, then run: ```sh TACTUS_AUTH_POSTGRES_URL=postgresql://localhost/tactus_auth node tests/e2e/dev-multi-user.mjs \ --configured-auth \ --host 100.86.255.76 \ --port 8878 \ --tls-cert /path/to/cert.pem \ --tls-key /path/to/key.pem \ --dist target/dev-ui ``` The launcher stores isolated engine data under `target/dev-data` by default. Pass `--data-dir` to use another directory. It disables automatic analysis in the served development shell. No authentication secret is sent to the frontend. For isolated local testing, replace `--configured-auth` with `--fixture`. This mode starts the signed test provider and offers named test identities. The fixture is for development only. ## Browser asset cache The client stores downloaded audio, waveform peaks, and analysis visualization JSON in IndexedDB. The cache holds up to 2 GiB per browser origin and evicts the least recently used files when it reaches that limit. Its keys include the engine URL, so data from different engines stays separate. On reload, the client validates each cached file with its ETag. Unchanged files reuse local bytes; rerun analyses and rewritten audio replace cached files. Deleted results and failed requests never fall back to stale cached data. The live track tree, job status, commands, and playback stream remain live. Audio decoding and visualization parsing still happen after a full reload. Browsers can evict stored data, and private browsing or storage limits can prevent caching. In those cases the client loads files from the engine normally. Clear this site's browser storage to remove the local cache. ## Verification Run the root Rust checks from this directory: ```sh cargo fmt --all -- --check cargo clippy --workspace --all-targets --locked -- -D warnings cargo test --workspace --locked ``` The DSP implementation and golden tests are working code. The checked-in fixtures and goldens live under `tactus-dsp/tests/`. Statistical key, chord, and segmentation tests require the `quality-tests` feature and run separately: ```sh scripts/test_dsp_quality.sh ``` See [Backend verification](docs/analysis-backends.md#verification) for targeted tests, optional oracle data, and the distinction between librosa parity and analysis quality. CI runs the root and UI checks in separate jobs. The `dsp-quality` workflow runs the statistical corpus on DSP-related changes. `tests/e2e/run.sh` builds a fresh UI bundle and runs Playwright against an existing engine. Set `TACTUS_ENGINE_BASE` explicitly for a local engine: ```sh TACTUS_ENGINE_BASE=http://localhost:5001 tests/e2e/run.sh ``` Without that variable, the runner uses the engine endpoint configured in the repository. Set `TACTUS_ENGINE_BASE` explicitly for your environment. `TACTUS_TEST_PORT` defaults to 8091. `RELEASE=1` enables optimized WASM for performance tests. A positional argument filters tests, for example `tests/e2e/run.sh gain`. ## Documentation - [Analysis backends](docs/analysis-backends.md): capabilities, configuration, execution boundaries, and tests. - [Immutable audio playback](docs/audio-playback-api.md): playable source catalog, blob identity, uploads, and retention. - [Server audio playback](docs/server-audio-playback.md) and [Playback clock](docs/playback-clocks.md): audio delivery and timeline ownership. - [Analysis playback and rship emitters](docs/live-analysis-emitters.md): routing, scheduling, and Rocketship identities. - [Measure rship latency](docs/rship-latency.md): pulse-to-subscription timing and the diagnostic echo action. - [All-In-One evidence rack](docs/all-in-one-evidence-rack.md): bounded display data and the timeline renderer. - [Source archive and permanent deletion](design/source-archive.md): archive, restore, purge, and recovery. - [Structure comparison](design/structure-comparison.md): comparing segmentation results. - [Development deployment](docs/dev-deployment.md): deploying engine and UI previews to the development host. The [May 2026 track-view proposal](docs/2026-05-26-track-view-redesign.md) is a historical design document, not a description of the current UI. To include the authentication restart check in the browser suite, set `TACTUS_TEST_AUTH_POSTGRES_URL` to a disposable PostgreSQL database before running `node tests/e2e/verify-multi-user.mjs`. The check signs in through the fixture, restarts the engine, restores the same session, checks REST and Myko authorization, then verifies logout survives another restart. Without this test setting, the suite explicitly uses in-memory sessions. ### Batch automation authentication Set `TACTUS_MACHINE_TOKEN_FILE` to a read-only mounted credential file to enable the batch runner API. Generate the credential with `openssl rand -hex 32`; the file must contain exactly 64 hexadecimal characters with an optional newline. It must be a regular file owned by root or the engine user, with no group or other permissions, such as mode `0600` or `0400`. A configured missing, malformed, symlinked, or overexposed file fails startup. Omit the variable to disable batch access. Rotate the file and restart the engine to replace the credential. Set `TACTUS_AUDIO_TOKEN_FILE` to a separate protected file with the same format and ownership rules to enable the audio consumer. All configured machine credentials must differ; identical credentials fail startup. Send the credential only in an `Authorization: Bearer ` header, using WSS for remote audio connections. Never put credentials in URLs. | Principal | Allowed operations | | --- | --- | | BatchRunner | Existing batch start, status, pause, library and artifact verification routes below | | AudioConsumer | `GET /api/machine/playback/live-audio` WebSocket upgrade only | | ConnectionMonitor | `GET /api/machine/connections/ws` read-only Myko subscriptions below | | Browser session | Existing browser routes and session revocation rules | Set `TACTUS_MONITOR_TOKEN_FILE` to a third protected credential file to enable the connection collector. The authenticated `/api/machine/connections/ws` socket preserves the existing Myko JSON/CBOR query protocol, initial snapshots, and reactive diffs. It accepts only `GetAllRshipConnections` with item type `RshipConnection`, `GetAllRshipSettingss` with item type `RshipSettings`, query cancellation, and ping. Query payloads use the native `QueryRequest` transaction and creation-time metadata; windowing and additional arguments are rejected by comparing the supplied fields with the registered type’s serialized result. At most the collector's two subscriptions can be active. Frames use Myko's standard message/frame bounds. Disallowed messages close the socket with code `1008`; slow readers that overflow the bounded response queue are disconnected rather than silently losing updates. The collector receives the existing records and IDs, including disconnected and disabled states, without synthesized connection status. It cannot issue commands, publish raw events, read other entities, use MCP, or acquire audio/batch/browser access. Use the bearer header over WSS or an explicitly verified encrypted private transport. Rotating the file requires restarting the engine, which closes existing monitor sockets. The audio route emits the same `TACTPCM1`, `TACTPCM2`, and `TACTCTL1` frames and `x-tactus-incarnation` header as browser playback. It grants no playback controls, clock/time reads, library access, batch operations, or generic MCP commands. Audio credentials last for the engine process lifetime; replacing a credential requires an engine restart, which closes existing audio sockets. Browser audio sockets still close when their session is revoked. Send `Authorization: Bearer ` over HTTPS or trusted local loopback HTTP. This principal can call only these routes: | Method | Route | Result | | --- | --- | --- | | GET | `/api/machine/library` | Sources, source folders, and memberships, including playback/blob identity | | POST | `/api/machine/profile-batches` | Existing `StartAnalysisProfileBatch` payload `{requestId, request}`; returns `{batchId}` | | GET | `/api/machine/profile-batches/{id}` | Lightweight progress counts, control, and active run count | | GET | `/api/machine/profile-batches/{id}?details=true` | Summary plus batch, executions, relevant source runs, output stems/analyses, and receipts/projections | | POST | `/api/machine/profile-batches/{id}/pause` | Pauses dispatch through the existing command; returns `{batchId, control}` | | GET, HEAD | `/api/machine/blobs/{blobId}` | Existing source-blob delivery, including range requests | | GET | `/api/machine/stems/{filename}` | Existing stem delivery, including range requests | | GET | `/api/machine/analysis/{analysisId}` | Existing analysis JSON delivery | Trusted internal agents can use the native MCP endpoint without a browser session when `TACTUS_INTERNAL_MCP_BIND_ADDR` is explicitly set, for example `0.0.0.0:5002` inside the container. This separate listener is disabled by default. Publish it only to the trusted VPN or private agent network; it grants the ordinary Tactus MCP query, analysis, authoring, and command capabilities without credentials. Only `/myko/mcp` is routed on that listener, using Myko's native HTTP, SSE, and MCP WebSocket transports. REST and the generic `/myko` WebSocket are unavailable there. Both listeners bind before workers start and share the server readiness gate. The public listener continues to require its existing browser authentication, including for `/myko/mcp`; forwarding headers do not grant private access. Machine credentials never grant generic Myko/MCP execution, browser login, rooms, uploads, deletion, playback mutations, or arbitrary commands. Browser authentication remains required for the existing application routes. Commands are attributed to `tactus-batch-runner`; the start command preserves existing validation, idempotency, profile selection, and capacity limits. A pause stops new dispatch while active work drains. Run the resource guard with `ops/monitor-profile-batch.py --base http://127.0.0.1:5001 --batch-id --token-file /etc/tactus/auth/batch-token --output `. It reads only the typed batch endpoint and requests the explicit pause operation when resource thresholds are crossed. It refuses to forward the credential through redirects or non-loopback plain HTTP. --- slug: /optik/readme title: optik source: optik --- # Optik External platform releases and validated deployment stacks are tracked in the [compatibility catalog](compatibility/README.md). Treat its named profiles as the source of truth for kernel, camera SDK, DeepStream, CUDA, TensorRT, driver, and container-runtime compatibility. Real-time multi-camera tracking, GPU-accelerated inference, camera calibration, and WebRTC streaming platform. ## Features - **Multi-Source Video** — RTSP, EVT (Emergent Vision), NDI, ONVIF discovery, file playback - **Camera Calibration** — Chessboard/ChArUco detection, automatic frame capture, intrinsic computation - [Color correction previews](docs/color-correction.md) with configurable card references, clean frame capture and synthetic test sources - **GPU Inference** — DeepStream 9.1 batched inference with Triton Inference Server - **WebRTC Streaming** — Per-viewer H.264 encoding, WHEP endpoints, WebGL rendering - **EVT Camera Control** — Discovery, parameters, PTP sync, preset management - **Web UI** — Leptos CSR + WASM with reactive myko entity state - **Declarative Pipelines** — GStreamer graphs as myko entities, reconciled by driver ## Quick Start ```bash sudo ./setup.sh # Install dependencies cp .env.example .env # Configure trunk serve # Terminal 1: frontend on :8080 cargo watch -x 'run -p optik-server' # Terminal 2: backend on :3000 ``` **EVT cameras** require root for Rivermax raw socket access: ```bash sudo -E env "PATH=$PATH" cargo watch -x 'run -p optik-server' ``` ### Docker ```bash docker compose up --build ``` Starts Optik (port 3000) with Triton Inference Server. Requires NVIDIA Container Toolkit. ## Requirements | Dependency | Path | Notes | |------------|------|-------| | Rust stable | -- | `rustup default stable` | | Trunk | -- | `cargo install trunk` | | DeepStream 9.1 | `/opt/nvidia/deepstream/deepstream-9.1` | GPU inference | | OpenCV 4.x | system | Calibration | | GStreamer 1.0 | system | Video pipelines | | CUDA 12 | system | GPU acceleration | | EVT SDK | `/opt/EVT/eSDK` | Optional: Emergent Vision cameras | | NDI SDK | `/opt/ndi-sdk` | Optional: NDI sources | The setup script handles most dependencies. Library paths configured via rpath in `.cargo/config.toml`. ## Data Persistent data in `./data/` (override with `OPTIK_DATA_DIR`): | Directory | Contents | |-----------|----------| | `config.json` | Server configuration | | `calibrations/{source_id}/` | Calibration frames (`frames/*.png`) and `metadata.json` | | `frame_sequences/` | Auto-discovered video frame sequences | | `cameras/` | Camera entity metadata | | `stages/` | Stage/rig definitions | | `pipelines/` | Saved inference pipeline configs | | `evt_settings/{source_id}/` | EVT camera presets | | `thumbnails/` | WebRTC preview thumbnails | | `graphs/` | GStreamer pipeline graphs (DOT + JSON) | --- ## Development ### Architecture ``` Video Sources (RTSP / EVT / NDI / ONVIF / File) │ Optik Server (Rust / Axum) ├── myko Reactive entity state (CellServer on :3001) ├── myko-gstreamer Pipeline entity → GStreamer reconciler ├── GStreamer Video pipelines ├── DeepStream GPU inference ├── Triton Model serving ├── WebRTC Streaming (WHEP) └── OpenCV Calibration │ Web Browser (Leptos CSR + myko-leptos live queries) ``` State flows: commands → entity mutations → hyphae subscriptions → GStreamer reconciliation → bus events → entity updates → UI live queries. ### Workspace | Crate | Purpose | |-------|---------| | `optik-types` | Entities + commands (WASM-safe, `#[myko_item]` + `#[myko_command]`) | | `myko-gstreamer` | Pipeline entity → GStreamer reconciler (driver) | | `triton-types` | Triton metric DTOs (WASM-safe) | | `triton-tools` | Triton client + nvinferserver config parsing | | `optik-server` | Axum backend, discovery, orchestration | | `optik-app` | Leptos UI components (CSR) | | `optik-deepstream` | DeepStream FFI bindings | | `optik-evt` | EVT camera SDK bindings | | `optik-opencv` | Calibration, detection, PnP | | `gstreamer-tokio` | Async GStreamer primitives | | `gstreamer-macros` | `pipeline!{}` proc macro | ### Build ```bash trunk serve # Frontend with hot reload (:8080) cargo watch -x 'run -p optik-server' # Backend with hot reload (:3000) trunk build --release # Production frontend → dist/ cargo build -p optik-server --release # Production server cargo test # Unit tests npx playwright test # E2E tests cd gst-nvevtcamsrc && make # Build EVT GStreamer plugin ``` ## License TBD --- slug: /pulse-pipe/docs/24-user-guide title: Pipe user guide and CLI reference source: pulse-pipe --- # pulse-pipe — User Guide & Command Reference > The living reference for everything pulse-pipe does today: concepts, every CLI command, > the DCC hubs, the daemon, caches, and how the pipe behaves with a whole team on it at > once. Unlike `docs/01–23` (point-in-time design records), this document describes the > **implemented** system. --- ## 1. What pulse-pipe is pulse-pipe is an asset pipeline: it moves geometry, textures, materials, and data between Houdini, Substance Painter, and Unreal Engine through **immutable, versioned publishes on shared storage**. Nothing is ever edited in place — every publish creates a new numbered version with a manifest recording what it is, who made it, and where it came from. Everything else (the CLI, the daemon, the Houdini and UE hubs) is a way to write, find, or consume those versions. Three interfaces, one engine: | Interface | What it's for | | --- | --- | | **`pipe` CLI** | publishing, browsing, lifecycle, project admin, bakes, launching DCCs | | **Houdini hub** | in-DCC panel: workfiles, load published versions, publish, see what's loaded | | **UE connector** | in-editor menu + hub: sync published bundles into `/Game/Pulse/...` | All three talk to the same storage layout on the NAS, and (except the CLI's direct mode) through a small local **daemon** that resolves addresses and streams change events. --- ## 2. The address — the one idea to internalize Every published thing has an address: ```text / / / [/ ] / ExampleProject / PanelGrid / tex / Marble_White / v007 ExampleProject / PanelGrid / mesh / static / lo / v003 ``` - **project** — the show (`ExampleProject`). Set once with `pipe use`; most commands then omit it. - **scope** — WHERE it lives: - an **asset** (PascalCase: `PanelGrid`) under an asset type (`env`, `prop`, `veg`, `arc`, `lgt`), - a **library scope** (lowercase: `surf`, `hdri`, `lut`) — shared across assets, - a **project scope** (e.g. `ref`) — project-wide reference material. - **kind** — WHAT type: `mesh`, `tex`, `material`, `data`, `bundle`, … Kinds are pluggable (`pipe kinds` lists what's installed); a kind's *adapter* can validate payloads, enforce slot names, and extract metadata. - **slot** — WHICH one: a mesh role (`static`, `skeletal`, `collision`, `lod`), a surface (`Marble_White`), a data table (`LedMapping`). - **variant** *(optional 6th axis)* — geometry variants of ONE asset: `hi`, `lo`, `voronoi`… Each variant has its own independent version stream. Omitted = the default variant, so pre-variant addresses stay valid forever. On disk the default lives in a `_default/` folder (see `pipe project migrate-variants`). - **version** — `v001`, `v002`, … Versions are immutable; "latest" always means the highest non-retracted version. **Idempotency:** publishing byte-identical content to a slot **reuses** the existing latest version instead of minting a new number (`--force-version` overrides). So re-runs of bakes and exports are free. **Statuses:** every version carries a lifecycle status — `wip` → `review` → `approved`, plus `retracted` (hidden from resolution but kept for audit). Every transition is recorded in the manifest's audit trail with who/when/why. **Manifests:** each version folder contains `manifest.json` — the address, per-file sha256 + sizes, operator, host, timestamps, status history, provenance facts from the publishing DCC, and *representations* (named file groups inside one publish, e.g. a mesh carrying both `fbx` and `bgeo`, or a texture publish carrying `maps` + `preview`). --- ## 3. Quick start ```text pipe use ExampleProject # activate the project + start the background daemon pipe whoami # who the pipe thinks you are + active project pipe ls # what exists (assets by type, library scopes) pipe ls PanelGrid # one asset's published slots + current versions pipe ingest PanelGrid mesh static model.fbx --type env # bring a file in pipe publish surf material Marble_White ./out # publish a folder pipe history PanelGrid mesh static # full audit trail pipe run houdini -d modeling --asset env/PanelGrid # work in Houdini ``` Machine setup is environment variables (rendered by pulse-deploy on the fleet; set manually on a dev box) — see §10. --- ## 4. Command reference Conventions: the active project comes from `pipe use`; addresses in commands are `///` (no project segment). `--variant` selects a geometry variant anywhere a slot is addressed; omitting it means the default variant. ### Context | Command | What it does | | --- | --- | | `pipe use [--no-daemon]` | Activate a project and start the warm background daemon (the thing hubs connect to). `--no-daemon` for scripts/CI. | | `pipe whoami` | Show operator identity + active project. | | `pipe kinds` | List installed kind adapters. | ### Getting content in **`pipe publish [options]`** Publish a file or directory as the next version. Options: `--variant hi` · `--status wip|review|approved` · `--source bake|ingest|generate` · `--note "…"` · `--label X` (repeatable) · `--force-version` · `--representations '[{"name":"fbx","files":[…]}]'` (named file groups). Single-file kinds (e.g. `mesh`) reject multi-file payloads unless grouped into representations; a `.meta.json` sidecar doesn't count against cardinality. Multi-piece meshes follow the `{SM|SK}_{Scope}[_{variant}][_NN].fbx` naming standard and are validated on publish. **`pipe ingest [options]`** Like publish, but for **external** files: renames the source to the kind's canonical name, records provenance (original path + hash, `--note`, `--units`). `--type env` declares the asset type when the ingest creates a brand-new asset. ### Finding things | Command | What it does | | --- | --- | | `pipe ls` | Overview — assets by type + library scopes. | | `pipe ls ` | One scope's published slots, each with current version + status. | | `pipe versions [--variant v]` | All versions of a slot with statuses. | | `pipe history [--variant v]` | Versions + who/when + the full status audit trail. | ### Lifecycle | Command | What it does | | --- | --- | | `pipe status [--reason "…"]` | Transition a version (`wip`/`review`/`approved`/`retracted`). Audited. | | `pipe retract [--reason "…"]` | Hide a version from resolution; bytes kept. Reversible. | | `pipe unretract ` | Revert to the previous non-retracted status. | ### Deleting & recovering | Command | What it does | | --- | --- | | `pipe purge [--reason] [--hard] [-y]` | Delete a version, slot, or whole asset. Default is **soft**: moved to the project's `_trash/` and logged (who/when/why). `--hard` removes bytes outright. | | `pipe trash` | List restorable items in `_trash`. | | `pipe trash restore ` | Restore item N to its original address. | | `pipe trash empty [-y]` | Permanently delete everything in `_trash`. | All trash operations are serialized against each other across machines and their audit log is corruption-proof; two people purging at once get one clean winner and one clean "nothing to purge". ### Bundles A **bundle** pins a set of member versions ("PanelGrid as of now") into a versioned `bundle.json` the UE connector consumes. Pins never move — a bundle is a snapshot. | Command | What it does | | --- | --- | | `pipe bundle create [--slot main] [--member kind/slot[=vNNN]]… [--variant v] [--note]` | Pin members NOW and publish `scope/bundle//vNNN`. Default = every published slot at latest; `--variant lo` builds "the lo view" (that variant per slot, default where a slot has none); identical member sets reuse the existing bundle version. Members retracted mid-creation are caught. A member that does not resolve refuses the whole create, itemised; `--allow-dangling` publishes anyway and records the unresolvable members in `bundle.json` (`pipe bundle check` then reports them as accepted rather than rotted). | | `pipe bundle list ` | Bundle streams + versions under an asset. | | `pipe bundle check [] [--latest] [--json]` | Report bundles whose pinned members no longer resolve (slot renamed, variant dropped, version purged, member retracted). Read-only; exit 0 clean · 1 problems · 2 can't tell. The purge log supplies the reason. | | `pipe bundle repin [--slot main] [--version vNNN] [--dry-run]` | Publish a NEW bundle version with dangling members re-pinned to the current version, or dropped where the slot/variant is gone. The source bundle is never touched. | ### Project administration | Command | What it does | | --- | --- | | `pipe project create [--code P-0616] [--asset-types …] [--library …] [--mode content\|shot] …` | Scaffold a project + write `project.yaml`. Idempotent. The name is immutable. | | `pipe project list` / `pipe project info` | List projects (active marked) / show a descriptor. | | `pipe project edit [--add-asset-type X] [--remove-library-scope Y] …` | Guarded edits — removals are blocked while content exists under them; concurrent edits from two machines both survive. | | `pipe project delete [--hard]` | Whole-project delete, soft to `_trash` by default. | | `pipe project migrate-variants [NAME] [--dry-run]` | One-time move of legacy `slot/vNNN` dirs into `slot/_default/`. Idempotent; readers fall back to the legacy layout until it's run. **Always `--dry-run` first** and read the report. | ### The daemon | Command | What it does | | --- | --- | | `pipe serve` | Run the daemon in the foreground (usually unnecessary — `pipe use` starts one detached). | | `pipe serve --status` | Is one running? (pid/port) | | `pipe serve --stop` | Stop it. | One daemon per machine. Racing launchers can't spawn duplicates and a second `pipe serve` refuses to clobber a healthy one. ### DCC launch **`pipe run houdini -d [--asset / | --shot /] [--hip file] [--project X] [--dry-run]`** Launches Houdini inside a work context: sets the work-area env (`$PP_WORK`, `$PP_WORKFILE_STEM`, entity/department vars), writes a per-launch session file (identity + daemon handshake), and opens the latest workfile (or `--hip`). `--dry-run` prints the plan. ### Texturing (Substance Painter) | Command | What it does | | --- | --- | | `pipe texturing list` | Bakeable surfaces in the SPSM library, by category. | | `pipe texturing status \| --all [--variant X] [--category Y] [--json]` | Which library surfaces are baked onto an asset, and whether they are still valid: **baked** (current), **outdated** (the material has a newer version), **old geo** (the asset's mesh has been republished since the bake), **missing**, **orphan**. | | `pipe texturing bake … --mesh-address [--resolution 8k] [--category X \| --all] [--missing] [--outdated] [--stale-geo] [--rebake-mesh] [--dry-run]` | Bake one/many surfaces in a **single persistent SP session**: SP launches once, mesh maps bake once (or come from cache), then each surface is applied → exported (D/N/H/RMA UDIM + preview) → validated → published to `tex/` with representations. One mesh per batch. | | `pipe texturing meshcache list \| prune [--shared] [--dry-run]` | Inspect the baked-base cache tiers and reclaim entries keyed under a superseded key schema (unreachable by any current bake). | **Staleness.** A texture is only valid for the geometry it was baked onto — mesh maps and painted masks live in one mesh version's UV space — so republishing an asset's mesh stales every texture on it. `status` reports that as **old geo** (naming both mesh versions); `bake --stale-geo` re-bakes exactly that set. A byte-identical mesh republish is *not* flagged: the bake's recorded FBX hash still matches the new version's, so the maps stand. The mesh-map bake result (the expensive part) is cached two ways: locally per machine (`~/.pipe/texcache/meshcache/`) and in the project's shared `_meshcache/` on the NAS, so the **first artist to bake a mesh pays; everyone else downloads the result in seconds**. The cache key covers the mesh content hash, the export resolution **and every bake setting that shapes the base** (baker set, bake parameters, mesh-map resolution, normal format, workflow, SP template) — so editing `bake.bakers` in `texture-export.yaml` invalidates it on its own, with no `--rebake-mesh` needed. `--rebake-mesh` still force-bypasses both tiers. `--dry-run` validates everything (including cache state: `hit` / `shared_hit` / `miss`) without launching SP. --- ## 5. Working in Houdini — the hub Launch via `pipe run houdini …`. The **pulse-pipe** hub has four tabs: - **Workfiles** — versioned `.hip` files for your asset×department context. *Save next version* allocates a new number, *Open latest / Open selected / New* do what they say. With PR #21 the tab is collaboration-safe: - two artists can never overwrite each other's save (version numbers are claimed atomically); - if a teammate saved a newer version while you worked, saving warns you before your save forks history; - the list shows `· open: mst@WS-02` when someone else has a version open; - every save records who/when/from-which-version in a `.meta.json` sidecar. - **Load** — browse published versions (any scope/kind/slot/variant) and load them into the scene. Loaded nodes are stamped with their address, so the pipe always knows what a scene contains. - **Publish** — publish from the scene (mesh export via ROPs, staged privately, facts gathered automatically) straight to a versioned address. - **Loaded** — everything pipe-loaded in the current scene, with up-to-date / OUTDATED badges computed against the latest published versions. The hub talks to the local daemon and refreshes automatically on publish events — including publishes from **other machines**. A red dot = daemon unreachable; the hub reconnects and re-adopts a restarted daemon by itself. --- ## 6. Working in Unreal — the connector The connector ships as `pulse-pipe-ue.zip` (release artifact) and self-activates via `UE_PYTHONPATH`. In the editor it adds a **Pipe** menu: - **Pipe → Hub…** — the pulse-pipe hub window: pick an asset + bundle version, see each member's content-aware status (**loaded / outdated / partial / untracked / missing**, with N/M piece counts for multi-piece meshes), select what to sync, watch progress, housekeeping (clear staging, remove asset). - **Pipe → Sync (Auto)** — one click: latest bundle of the project's asset, staged and imported with no UI. How a sync works: resolve each bundle member via the local daemon → stage files to `Saved/PulseStaging//…` → import per kind (`mesh` → Static/Skeletal per slot (+ per-variant subfolders), `tex` → UDIM virtual textures, `data` → loose CSV under `Content/Pulse//Data`) → record a **receipt** (`Content/Pulse//.pulse/receipt.json`). Reliability properties worth knowing: - **Resumable**: multi-piece meshes import piece-by-piece, skipping pieces that already exist — interrupt a sync and the next one finishes only what's missing. - **Memory guard**: before each member (and each mesh piece) the connector checks commit headroom and forces a UE garbage-collect when low, so big 8K sets don't OOM the editor monotonically. - **Honest receipts**: crash-proof receipt writes; a member whose pieces half-failed is recorded `complete: false` and shows as **partial** — a sync can never claim "done" over missing pieces. - **Live updates**: a ⚡ banner + refreshed version lists when anyone — any machine — publishes; the event stream reconnects automatically after daemon restarts. - **Two editors on one project copy** don't corrupt each other's receipts or delete each other's in-flight staging. - Syncs are fast on big sets: file checksums for receipts come from the publish manifest instead of re-reading staged bytes. Headless `-game`/nDisplay nodes: the connector detects headless mode and never installs its editor UI tick — render-wall performance is unaffected. --- ## 7. The daemon, events, and multi-machine behavior Each machine runs one `pipe serve` daemon (started by `pipe use`), bound to `127.0.0.1` on a dynamic port with a per-daemon token — the handshake lives in `%USERPROFILE%\.pipe\daemon.json`. **`PIPE_HOME` must stay machine-local** (the pipe warns if it points at a network share). Routes (for tool authors): `GET /health`, `/list`, `/resolve`, `/kinds`, `/project`, `/events` (SSE stream); `POST /publish`, `/asset`, `/bundle`, `/status`, `/unretract`, `/notify`. `/health` reports both process liveness and **actual storage reachability**. **Cross-machine events**: every publish/status change drops a small JSON note in `/_events/`; every daemon tails that folder (2s) and re-broadcasts other machines' events to its local hubs. Result: artist B's Houdini and UE panels update within ~2–4s of artist A's publish, with no central server. Notes self-delete after 15 minutes. Kill switch: `PIPE_EVENT_JOURNAL=0`. **Who did what**: operator identity resolves as `PIPE_OPERATOR` env override → your **Plastic login** (`cm whoami` — per-artist even on the shared Administrator login) → OS user (with a warning if that's a shared login). The identity lands in every manifest, status change, and workfile sidecar. **Concurrency guarantees** (the short version — all enforced, all tested): | Situation | Behavior | | --- | --- | | Two machines publish the same slot simultaneously | Each gets its own version number; files never mix | | A publish crashes midway | No half-versions ever visible; leftover staging is skipped and swept — the slot never blocks | | Two machines change one version's status | Both audit entries survive | | Two machines create the same asset name under different types | One clean winner; the asset can't end up ambiguous | | Two artists save the same workfile line | Distinct versions, fork warning, no silent overwrite | | Purge/restore/empty race each other or a reader | Serialized; the restore log can't corrupt | --- ## 8. Storage layout (what's actually on the NAS) ``` / (PIPE_PROJECTS_ROOT) └── ExampleProject/ ├── project.yaml # the project descriptor ├── assets/// │ ├── publish////vNNN/ # immutable versions (+ manifest.json) │ └── work//houdini/{scenes,cache}/ # workfiles (versioned .hip + sidecars) ├── lib////… # library publishes (surf, hdri, lut) ├── ref/… # project-scope publishes ├── _trash/ # soft-purged items + audit logs ├── _events/ # cross-machine event notes (self-cleaning) └── _meshcache/ # shared baked-base .spp cache (team DDC) ``` Per-machine state lives under `~/.pipe/` (`daemon.json`, `session.yaml` = active project, `sessions/` = per-DCC-launch context, `texcache/` = local bake cache + staged meshes). --- ## 9. Kinds shipped today | Kind | Cardinality | Notes | | --- | --- | --- | | `mesh` | single artifact (+ sidecar; multi-piece via `fbx` representation) | closed slots: `static`, `skeletal`, `collision`, `lod`; naming validated | | `tex` | open | UDIM texture sets; baked publishes carry `maps` + `preview` groups | | `material` | single | `.spsm` smart materials | | `data` | single | CSV tables; adapter validates shape + extracts row counts | | `bundle` | single | pinned member snapshots (created via `pipe bundle`, consumed by UE) | Kinds are extensible: adapters register via the `pipe.commands`/adapter entry points, and `pipe kinds` shows what's live. --- ## 10. Environment variables | Variable | Default | Meaning | | --- | --- | --- | | `PIPE_PROJECTS_ROOT` | dev NAS (warns when defaulted) | The shared projects root. Set on every real deployment. | | `PIPE_HOME` | `~/.pipe` | Per-machine state dir. **Must be machine-local** (warned otherwise). | | `PIPE_OPERATOR` | *(unset)* | Identity override; otherwise Plastic `cm whoami`, then OS user. | | `PIPE_EVENT_JOURNAL` | `1` | `0` disables cross-machine event notes. | | `PIPE_MESHCACHE_SHARED` | project `_meshcache/` | `0` disables the shared bake cache; a path overrides its location. | | `PIPE_SP_EXE` / `PIPE_SP_PORT` | Adobe default / `60041` | Substance Painter executable / scripting port. | | `PIPE_SP_SMART_MATERIALS` / `PIPE_SP_TEMPLATE` | dev-box Dropbox paths | SPSM library dir / lean SP project template. | | `PIPE_TEXTURING_CACHE` | `~/.pipe/texcache` | Local bake cache root (meshcache, staged meshes, run dirs). | | `PIPE_SESSION` | *(set by `pipe run`)* | Per-launch session id the DCC reads its context from. | --- ## 11. Troubleshooting - **"in-flight publish dir already present" / stuck slot** — historical only; since PR #15 leftover staging never blocks a slot (skipped, swept after 24h). - **Hub shows stale versions** — pre-PR #19 behavior was manual refresh; after it, check `pipe serve --status` and the `_events/` folder exists on the share. - **Everything is attributed to `adm`** — you're on a shared login without Plastic: log into Plastic (`cm whoami` should print you) or set `PIPE_OPERATOR`. - **UE hub says "partial"** — some mesh pieces failed on the last sync (reason is in the receipt/log); just sync again — only missing pieces import. - **SP bake fails to load the mesh** — SP can't read the NAS share by design; the pipe stages meshes locally, so check `PIPE_TEXTURING_CACHE` disk space. - **Daemon won't start: "already running"** — that's the incumbent guard; reuse the running daemon or `pipe serve --stop` first. - **Houdini shows `C:\host\…` paths** — a backslash UNC leaked in; the pipe hands Houdini forward-slash paths everywhere — report where it came from. - **NAS files won't delete from Windows** — dev-share ACL quirk (files owned by root); purge handles it (rename-first), manual cleanup may need the NAS host. --- slug: /pulse-deploy/overview title: Overview source: pulse-deploy --- # Pulse Deploy Pulse Deploy provisions and configures Pulse infrastructure with Terraform and Ansible. It covers compute hosts, virtualization, storage, networking, render-host configuration, CI runners, and supporting services. ## Responsibilities Terraform manages infrastructure resources. Ansible converges host and service configuration, bootstraps PulseNode, and supports diagnostics, maintenance, and repair. [Pulse Cluster](/pulse-cluster/overview) handles Unreal render-fleet operations through `pulse cluster`. Cluster recipes, project materialization, launch, stop, restart, and redeploy belong to Pulse Cluster. ## Documentation - [Infrastructure operations](/pulse-deploy/operations) describes convergence, diagnostics, and repair. - [Automation](/pulse-deploy/automation) explains the boundaries between Pulse Deploy, Pulse Cluster, PulseNode, and Tachyon. - [Reference deployment](/pulse-deploy/reference-deployment) describes a deployment's infrastructure layout. Use the current [pulse-deploy repository](https://github.com/ignition-is-go/pulse-deploy) for inventory, playbooks, roles, and deployment-specific instructions. --- slug: /pulse-deploy/operations title: Infrastructure operations source: pulse-deploy --- # Infrastructure operations Pulse Deploy handles infrastructure configuration, diagnostics, maintenance, and repair. Use [Pulse Cluster](/pulse-cluster/overview) for routine Unreal render-fleet operations. ## Convergence and maintenance Use the current inventory and repository instructions to select the hosts and configuration involved. Terraform manages infrastructure resources; Ansible applies host and service configuration. Ansible can bootstrap or repair PulseNode installations. Routine exact-version PulseNode updates use typed host operations through `pulse cluster`. See the [pulse-deploy repository instructions](https://github.com/ignition-is-go/pulse-deploy/blob/main/AGENTS.md) for the current inventory, supported playbooks, and maintenance procedures. ## Render-fleet operations Use `pulse cluster` to inspect fleet state, edit recipes, select revisions, and submit lifecycle operations. Ansible does not launch, stop, restart, redeploy, or materialize clusters. Start with [Pulse Cluster](/pulse-cluster/overview). The [automation boundaries](/pulse-deploy/automation) describe which system owns each operation. ## Deployment context The [reference deployment](/pulse-deploy/reference-deployment) documents infrastructure layout. Confirm its details against the target environment's current inventory before use. --- slug: /pulse-deploy/reference-deployment title: Reference deployment source: pulse-deploy --- # Reference deployment [GPU Servers](https://www.gigabyte.com/Enterprise/GPU-Server/G493-ZB1-AAP1-rev-3x) efficiently consolidate generic compute resources. [Proxmox](https://www.proxmox.com/en/) and [Red Hat OpenShift](https://www.redhat.com/en/technologies/cloud-computing/openshift) are hypervisor and datacenter management tools which virtualize and pool compute resources, enabling software-defined configurations of VMs and containers. [Terraform](https://developer.hashicorp.com/terraform) and [Ansible](https://docs.ansible.com/) are the infrastructure-as-code libraries which define resource mappings, automatically provision virtual machines and containers, and define the operational command syntax for running the system. The majority of infrastructure work to date has been enabling the high-performance GPU and networking technologies necessary for virtual production and camera tracking (Rivermax, GPUDirect, PTP) to run in fully virtualized environments. ## Deployment configurations Three configurations: - **dev** — pre-onsite integration. Scaled-down rendering, day-to-day operations, telemetry. - **prod** — onsite integration. Full rendering configuration, telemetry, onsite development tooling. - **install** — final handoff. Reallocates development resources for additional failover headroom. Specific inventories live in [pulse-deploy](https://github.com/ignition-is-go/pulse-deploy). ### dev Scaled-down configuration out of a single rack. #### Compute (rendering / inference) - 2× [Dev GPU Servers](https://www.gigabyte.com/Enterprise/GPU-Server/G493-ZB1-AAP1-rev-3x) - 1× dual-socket [AMD EPYC 9575F 64-core CPU](https://www.amd.com/en/products/processors/server/epyc/9005-series/amd-epyc-9575f.html) (128 cores total) - 12× Samsung DDR5 96GB RAM (1.1TB total) - 8× [RTX 6000 Ada GPUs](https://www.nvidia.com/en-us/products/workstations/rtx-6000/) per server - 2× [ConnectX-6 NICs](https://www.nvidia.com/en-sg/networking/ethernet/connectx-6/) #### Control, DevOps, artist workstations - 2× general-purpose servers - 1× [AMD EPYC 9474F 48-core CPU](https://www.amd.com/en/products/processors/server/epyc/4th-generation-9004-and-8004-series/amd-epyc-9474f.html) - 4× DDR5 64GB RAM (256GB total) - 2× [RTX 4090 GPUs](https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/rtx-4090/) - 1× [BlueField-2 DPU](https://www.nvidia.com/en-us/networking/products/data-processing-unit/) ### prod #### Compute (rendering / inference) - 5× [Prod GPU Servers](https://www.gigabyte.com/Enterprise/GPU-Server/G494-ZB0-AAP2) - 1× dual-socket [AMD EPYC 9575F 64-core CPU](https://www.amd.com/en/products/processors/server/epyc/9005-series/amd-epyc-9575f.html) (128 cores total) - 12× Samsung DDR5 128GB RAM (1.5TB total) - 8× [RTX 6000 Blackwell GPUs](https://www.nvidia.com/en-us/products/workstations/professional-desktop-gpus/rtx-pro-6000-max-q/) per server - 2× [BlueField-3 DPUs](https://www.nvidia.com/en-us/networking/products/data-processing-unit/) GPU servers run Unreal content/previs, TouchDesigner, and the DeepStream computer vision pipeline. Because the CPUs are dual-socket, the NUMA topology trees out as follows: - CPU Socket 0 (NUMA 0) - Cores 0-63 - GPUs 0-3 - BF3 card 0 - 4× VM slots: 16 cores, 128GB RAM, 1 GPU, 2 NIC VFs (2110/storage) - CPU Socket 1 (NUMA 1) - Cores 64-127 - GPUs 4-7 - BF3 card 1 - 4× VM slots: 16 cores, 128GB RAM, 1 GPU, 2 NIC VFs (2110/storage) Precise VM responsibility mapping and failover strategies are TBD, but because the hardware topology is homogeneous, VM responsibilities can crisscross across servers for resilience. #### Control (Rocketship) - 10× Control Cluster Nodes — [Gigabyte B343-C40-ACJ3 (AAJ3 Rev 3)](https://www.gigabyte.com/Enterprise/High-Density-Server/B343-C40-AAJ3), 3U 10-node EPYC 4005 chassis - 1× [AMD EPYC 4565P](https://www.amd.com/en/products/processors/server/epyc/4005-series/amd-epyc-4565p.html) 16-core CPU (4.3GHz, 170W) per node - 2× 16GB DDR5 ECC UDIMM 5600MHz per node (32GB per node, 320GB total) - 1× 960GB PCIe 4.0 M.2 2280 boot drive per node - 2× 3.84TB PCIe Gen 5 2.5" 15mm NVMe per node (7.68TB per node, 76.8TB total) - 1× [Broadcom BCM57416](https://docs.broadcom.com/doc/957416A4160C-DS) 2-port 25G NIC per node - 3× [Broadcom N425G](https://www.broadcom.com/products/ethernet-connectivity/network-adapters/n425g) 4×25G SFP28 OCP3.0 multi-host NIC (shared across nodes) The control cluster scales Rocketship horizontally across nodes. Each node runs a copy of the rship server process in a Docker Swarm configuration. #### Compositing (TBD) Plans to run a compositing server to receive Rivermax streams and output to the pixel processor for keep-alive and final output control. ## Network Three subnets split traffic by role: management, media (SMPTE 2110, VLAN 20 on the media fabric switch), and storage. - **SR-IOV** — ConnectX-6 (prod) and BlueField-2 / 3 (dev) NICs partitioned into Virtual Functions, each passable to a guest. CX6 in prod gives 16 VFs per card, 32 per host. - **Uplinks** — single PF per CX6 card; OVS auto-detects the live link. No LAG. - **PTP** — Symmetricom grandmaster, domain 0, UDP E2E. Sub-microsecond clock sync underwrites Rivermax frame pacing. - **Multicast** — IGMP snooping at OVS bridges so 2110 streams reach only their subscribed receivers. - **Jumbo frames** — MTU 9000 end-to-end (guest → VF → PF → OVS). A single 1500-MTU hop fragments and drops Rivermax frames. - **GPUDirect** — zero-copy DMA between GPU memory and NIC requires both on the same PCIe root complex on the GPU server, and Resizable BAR enabled on the GPU. ## Storage - **Per-node ZFS** — each Proxmox host has local NVMe pools (`zfs-nvme-XX`); not shared across nodes. - **NAS** — the storage server hosts SMB / NFS shares for installers, project content, and Proxmox Backup Server. --- slug: /pulse-deploy/automation title: Automation source: pulse-deploy --- # Automation Pulse separates infrastructure convergence from Unreal cluster lifecycle. Use the owner of the operation you need. | Responsibility | Owner | | --- | --- | | Infrastructure provisioning | Pulse Deploy Terraform configuration. | | Host and service convergence, diagnostics, repair, and bounded maintenance | Pulse Deploy Ansible configuration and playbooks. | | Cluster recipes and admitted lifecycle operations | Pulse Cluster and the `pulse cluster` CLI. | | Render-host lifecycle and project materialization | Typed PulseNode actions dispatched by Pulse Cluster. | | Generic admitted Ansible run execution | Tachyon's typed run contracts, executor, and runner. | | Agent access to application operations | The connected application's Myko MCP endpoint. | The [reference deployment](/pulse-deploy/reference-deployment) describes the infrastructure. Verify deployment details against the current inventory and repository instructions before use. ## Unreal cluster operations Pulse Cluster stores durable recipes and coordinates explicit operations against the render fleet. Editing a recipe does not itself launch Unreal. Observers report live state without automatically repairing a cluster. Production render hosts form a render pool. Content and previs roles are selected at launch time by the typed cluster definition; they are not standing inventory groups. Use `pulse cluster --help` for commands available in the installed release. Read the [Pulse Cluster documentation](/pulse-cluster/overview) for recipe, revision, deployment, and observation workflows. Ansible does not launch, stop, restart, redeploy, or materialize a cluster. Missing native capabilities fail the operation rather than selecting an Ansible fallback. ## Infrastructure work Use the current [pulse-deploy repository instructions](https://github.com/ignition-is-go/pulse-deploy/blob/main/AGENTS.md) for inventory, convergence, repair, and maintenance. The inventory describes the available hosts. Pulse Cluster owns runtime cluster intent. Terraform manages infrastructure resources. Ansible converges host and service configuration and bootstraps PulseNode through the Cluster CLI's enrollment transaction. ## Agent access Use [Myko MCP](/myko-mcp/overview) to discover the operations registered by a connected application. The MCP tools `connection_status`, `search`, and `execute` provide connection inspection, operation discovery, and execution. Discover the current operation contract before calling it. For Unreal lifecycle operations, use Pulse Cluster. A historical `pulse-deploy-mcp` tool catalog does not describe the current fleet API. Operational workflows in Pulse Deploy's `pulse-skills/` package provide repository-specific investigation guidance. Read the installed skill and confirm the tools exposed in the active session. For documentation endpoints, agent-tool responsibilities, and repository work, see [Docs for agents](/pulse/docs-for-agents). ## Generic Ansible execution [Tachyon](https://github.com/ignition-is-go/tachyon) supplies typed contracts for admitted Ansible runs, streams process output, and reports outcomes. The consuming application owns dispatch policy and domain entities. A cancellation request does not prove that a claimed run has stopped. Follow the run's terminal status before treating its resources as released. --- slug: /pulse-cluster/overview title: Pulse Cluster source: pulse-cluster --- # Pulse Cluster Pulse Cluster is the operational control plane for Pulse's Unreal Engine render fleet. It stores launch recipes, admits explicit lifecycle operations, coordinates PulseNode actions, and combines desired state with live fleet observations. ## Recipes and operations A cluster recipe describes what to run. Editing a recipe does not launch Unreal or change the running deployment. An explicit lifecycle operation applies the selected recipe. Use `pulse cluster` to inspect hosts and clusters, select project revisions, preview deployments, and launch, stop, restart, or redeploy a cluster. PulseNode executes typed actions on the hosts and reports their state. Observers report drift without automatically restarting a cluster. ## Install and connect [Pulse CLI](/pulse-cli/overview) installs the Cluster component: ```sh pulse components install cluster pulse cluster myko set http://MYKO_HOST:5157 pulse cluster status ``` Replace `MYKO_HOST` with the server for your environment. Use `pulse cluster --help` for the installed command surface. ## Inspect and operate ```sh pulse cluster host list pulse cluster status pulse cluster describe pulse cluster diff pulse cluster logs ``` For an existing recipe, preview a redeployment before submitting it: ```sh pulse cluster redeploy --dry-run pulse cluster redeploy --watch ``` `--dry-run` validates and previews without dispatching the operation. `--watch` follows the submitted operation. Interrupting the local CLI does not cancel the server-side operation. ## Infrastructure boundary [Pulse Deploy](/pulse-deploy/overview) provisions and configures the infrastructure and bootstraps PulseNode. Pulse Cluster owns cluster lifecycle and project materialization. Ansible is not a lifecycle fallback. ## Further reading The [pulse-cluster repository](https://github.com/ignition-is-go/pulse-cluster) contains the current operator workflows and implementation. - [CLI reference](https://github.com/ignition-is-go/pulse-cluster/blob/main/pulse-cluster-cli/README.md) - [Architecture and operation contracts](https://github.com/ignition-is-go/pulse-cluster/blob/main/SPEC.md) --- slug: /pulse-agents/overview title: Pulse Agents source: pulse-agents --- # Pulse Agents Pulse Agents runs durable agent sessions in remote execution environments. The Console and `pulse agents` CLI connect to the same control plane for launching, attaching to, and inspecting sessions. ## Console and sessions The [Pulse Agents Console](https://agents.rship.io) is the browser interface. Each pane can attach to an existing session or launch a new one. Pane layout belongs to the browser; sessions and transcripts belong to the server. Closing a pane or browser does not end the agent session. A new session selects an execution environment, repositories, and an optional Levi task. Pulse Agents creates or selects an isolated checkout through `pulse work`, then launches the selected harness configuration. The control plane owns session state, transcripts, approvals, files, diffs, terminals, and recovery. An environment is a launch target. Its advertised presets identify compatible harness configurations. Available environments and presets depend on the deployed version and account readiness. ## CLI The [Pulse CLI](/pulse-cli/overview) installs and updates the Pulse Agents component: ```sh pulse components install pulse-agents pulse components update pulse-agents ``` The terminal interface provides authentication, environment discovery, and a session dashboard: ```sh pulse agents auth login pulse agents auth status pulse agents environments pulse agents ``` The dashboard lists sessions, supports attachment to their terminals, and provides a new-session workflow. Detaching returns to the dashboard without ending the session. `pulse agents --help` describes the installed command surface. Signing in to Pulse Agents and connecting a GitHub account for repository access are separate actions. ## Related tools | Product | Responsibility | | --- | --- | | Pulse Agents | Durable remote session execution, interaction, and recovery. | | [Marshal](/marshal/readme) | Live agent presence and messaging across sessions. | | [Levi](/levi/readme) | Tasks, dependencies, claims, and completion tied to Git ancestry. | | [Pulse CLI](/pulse-cli/workspaces) | Isolated repository checkouts and workspace lifecycle. | | [Myko MCP](/myko-mcp/overview) | Discovery and execution of a connected application's operations. | See [Docs for agents](/pulse/docs-for-agents) for reading documentation and starting repository work. ## Source and deployment The [pulse-agents repository](https://github.com/ignition-is-go/pulse-agents) contains the current implementation, configuration, and development instructions. The repository and release images are private. Session availability and supported harness configurations come from the deployed control plane. --- slug: /marshal/readme title: Marshal source: marshal --- # marshal A coordination service that lets Claude Code, Codex, Pi, Prime Agent, and opencode sessions—on one machine or spread across a network—see each other and pass messages. After install, the `roster` resource shows every live session, `send_message` reaches them, and harness-specific delivery surfaces inbound peer messages. ## Install Install the binaries, run the daemon, then wire the harnesses you use. ### 1. Install the binaries ```bash cargo install marshal-shim marshal-daemon # optional but recommended for visibility into the live roster cargo install marshal-tui ``` This puts `marshal-shim` and `marshal-daemon` on your `PATH` (typically `~/.cargo/bin`). ### 2. Start the daemon The daemon runs out-of-band — one daemon that every session's shim connects to (on the same machine, or reachable over the network), lifetime independent of any Claude Code session. Pick whichever fits: ```bash # foreground in its own terminal marshal-daemon # or backgrounded under your shell job control marshal-daemon & # or under your favorite supervisor (systemd user unit, launchd, tmux pane, ...) ``` The daemon binds `0.0.0.0:6155` by default (so peers on other hosts can reach it without remembering to flip a flag) and writes its event log to `~/.local/state/marshal/events.jsonl`. Restrict to localhost only with `MARSHAL_BIND=127.0.0.1:6155`. ### 3. Add the plugin in Claude Code ```text /plugin marketplace add ignition-is-go/marshal /plugin install marshal-shim@marshal ``` Restart the session. From here on, every Claude Code instance pointed at the daemon sees the marshal MCP server and can talk to its peers. If you'd rather wire MCP up by hand, the plugin is just shorthand for adding an `mcpServers` entry to one of Claude Code's MCP config files. Pick the scope that fits: **Project scope** — write a `.mcp.json` at the repo root (commit it to share with collaborators): ```json { "mcpServers": { "marshal": { "command": "marshal-shim" } } } ``` **User scope** — applies to every Claude Code session for your user. Add a top-level `mcpServers` entry to `~/.claude.json`: ```json { "mcpServers": { "marshal": { "command": "marshal-shim" } } } ``` Or let the CLI do it for you: ```bash claude mcp add -s user marshal marshal-shim ``` > Note: `mcpServers` is **not** a supported key in `~/.claude/settings.json` — adding it there silently does nothing. It must live in `.mcp.json` (project) or `~/.claude.json` (user). ### 4. Allow the channel notifications (research-preview workaround) Marshal pushes peer messages into your transcript via Claude Code's experimental `notifications/claude/channel` capability. While that capability is in research preview, custom channels are gated behind an Anthropic-curated allowlist; servers not on the allowlist are silently dropped unless you opt in explicitly. Until marshal is on the official list, launch Claude Code with one of: ```bash # If you installed via the plugin (recommended) claude --dangerously-load-development-channels plugin:marshal-shim@marshal # If you wired the MCP server directly via .mcp.json claude --dangerously-load-development-channels server:marshal ``` Both forms key on the same name you set: `plugin:@` for plugin installs, `server:` for direct wiring. Set whichever fits as a shell alias to make it permanent (e.g. `alias claude='claude --dangerously-load-development-channels plugin:marshal-shim@marshal'`). Without the flag, `roster` and `send_message` still work — peer messages just don't surface as live `` blocks in your transcript. With the flag, every peer's `send_message` arrives as an inline notification while you're working. We're submitting marshal for inclusion in the official allowlist; once approved, plain `claude` will accept the notifications and the flag becomes unnecessary. ### Pi: native extension After starting a reachable daemon, install the published [`@agent-marshal/marshal-pi`](https://www.npmjs.com/package/@agent-marshal/marshal-pi) package: ```bash pi install npm:@agent-marshal/marshal-pi ``` Restart Pi. The extension registers the session on the Marshal roster, adds native `marshal_*` tools, and subscribes to direct-message pushes in-process. Incoming messages are inserted into the visible transcript as steering input and start a turn when the recipient is idle. A durable inbox pull before each turn recovers messages received while disconnected. No MCP shim or launcher wrapper is required. To use a remote daemon: ```bash export MARSHAL_DAEMON_ADDRESS=ws://:6155 pi ``` Use `/marshal-status` in Pi to verify the connection. See [`plugins/marshal-pi/README.md`](plugins/marshal-pi/README.md) for the complete tool, configuration, and development reference. ### Prime Agent: live conversation bridge Install the Prime Agent extension package after starting a reachable daemon: ```bash prime-agent package install npm:@agent-marshal/marshal-prime-agent ``` Restart Prime Agent. The extension registers each top-level conversation on the roster, adds native `marshal_*` tools, and subscribes to direct-message pushes in-process. RLM sub-agents remain part of their parent's Marshal session instead of appearing as separate roster entries. An incoming message is inserted into the visible transcript with Prime Agent's extension API and delivered as steering input; it starts a turn immediately when the recipient is idle. Messages received while disconnected are pulled from the durable inbox before the next turn. This is a dedicated Prime Agent package, separate from `marshal-pi`; no MCP shim or launcher wrapper is needed. To use a remote daemon: ```bash export MARSHAL_DAEMON_ADDRESS=ws://:6155 prime-agent ``` Use `/marshal-status` in Prime Agent to verify the connection. See [`plugins/marshal-prime-agent/README.md`](plugins/marshal-prime-agent/README.md) for tools, development loading, and delivery details. ### Codex: local setup without Ansible These steps give a laptop, workstation, or manually managed VM the same Marshal behavior as an infrastructure-managed host. You need a reachable `marshal-daemon`: use the daemon from step 2 above, or substitute the address of a shared daemon. Install the Codex CLI itself first and confirm that `codex` is on `PATH`. #### 1. Install and wire the shim On Linux x86-64 or macOS, the release installer downloads the correct shim, copies it to `~/.local/bin`, adds the Marshal MCP server and lifecycle hooks to `~/.codex/config.toml`, seeds the coordination instructions in `~/.codex/AGENTS.md`, and pre-trusts the hooks: ```bash curl -fsSL \ https://github.com/ignition-is-go/marshal/releases/latest/download/install-codex.sh \ | sh -s -- --daemon ws://:6155 ``` On Windows x64, run this from PowerShell: ```powershell $download = Join-Path $env:TEMP 'marshal-shim.exe' Invoke-WebRequest ` -Uri 'https://github.com/ignition-is-go/marshal/releases/latest/download/marshal-shim-x86_64-pc-windows-gnu.exe' ` -OutFile $download & $download codex-setup --daemon 'ws://:6155' ``` Windows installs the stable copy under `%LOCALAPPDATA%\marshal\bin` and adds that directory to the user `PATH`. Restart the shell after setup. Both commands are idempotent; rerun them to refresh the shim, hooks, or daemon address. For a source build, install `marshal-shim` with Cargo and run the same setup command: ```bash cargo install marshal-shim marshal-shim codex-setup --daemon ws://:6155 ``` A normal `codex` launch now gets the Marshal tools and pulls direct messages at prompt and tool boundaries. For a message to start a turn while Codex is idle, the interactive CLI must run through: ```bash marshal-shim codex-run [CODEX_ARGS...] ``` On Linux and macOS this attaches the TUI to Codex's managed app-server. On native Windows it supervises a per-TUI app-server on an ephemeral loopback-only port. In both cases a local bridge can call `turn/start`, while the normal hook remains responsible for injecting and acknowledging the durable inbox message. The launcher waits for the bridge's lifecycle subscription before attaching the TUI, so `thread/started` registers new and resumed sessions on the Marshal roster before their first prompt. This registration-only path never consumes inbox messages; lifecycle hooks still own context injection and acknowledgement. After Codex or the user names the selected thread, the bridge prefixes the title with its stable Marshal handle, for example `[sunny-summit] Fix reconnect handling`. Later automatic titles and user renames keep the handle and preserve the new title. The bridge does not name an empty thread, because doing so would prevent Codex from generating its normal title. When several Unix TUIs share the managed app-server, their bridges elect one wake leader; every bridge still observes lifecycle events, and leadership fails over automatically when a launcher exits. Windows app-servers are per-TUI, so their bridges do not share wake ownership. #### 2. Make idle wake the default for interactive Codex Codex does not currently expose the shared app-server choice as a `config.toml` setting, so make `codex-run` the interactive shell launcher. Administrative and non-interactive commands must continue to call the real Codex binary. For Bash, add the following to `~/.bashrc`. For Zsh, add it to `~/.zshrc` and replace `type -P codex` with `whence -p codex`: ```bash export MARSHAL_CODEX_BIN="$(type -P codex)" codex() { local arg direct=0 for arg in "$@"; do case "$arg" in exec|e|review|login|logout|mcp|plugin|mcp-server|app-server|remote-control|completion|update|doctor|sandbox|debug|apply|a|archive|delete|unarchive|cloud|exec-server|features|help|-h|--help|-V|--version|--remote|--remote=*) direct=1 ;; esac done if [ "$direct" -eq 1 ]; then "$MARSHAL_CODEX_BIN" "$@" else "$HOME/.local/bin/marshal-shim" codex-run "$@" fi } ``` For Windows PowerShell, add this to `$PROFILE.CurrentUserAllHosts` after opening a fresh shell: ```powershell $script:MarshalCodexBin = (Get-Command codex -CommandType Application).Source $script:MarshalShimBin = (Get-Command marshal-shim -CommandType Application).Source function global:codex { $directCommands = @( 'exec', 'e', 'review', 'login', 'logout', 'mcp', 'plugin', 'mcp-server', 'app-server', 'remote-control', 'completion', 'update', 'doctor', 'sandbox', 'debug', 'apply', 'a', 'archive', 'delete', 'unarchive', 'cloud', 'exec-server', 'features', 'help', '-h', '--help', '-V', '--version' ) $direct = $false foreach ($argument in $args) { $value = [string]$argument if ( $directCommands -contains $value -or $value -eq '--remote' -or $value.StartsWith('--remote=') ) { $direct = $true break } } if ($direct) { & $script:MarshalCodexBin @args return } $previous = $env:MARSHAL_CODEX_BIN try { $env:MARSHAL_CODEX_BIN = $script:MarshalCodexBin & $script:MarshalShimBin codex-run @args } finally { $env:MARSHAL_CODEX_BIN = $previous } } ``` Start a new shell, then run plain `codex` or `codex resume`. An interactive session should have both a `marshal-shim codex-run` launcher and a `marshal-shim codex-bridge` process, and it should appear in the Marshal roster as soon as the TUI opens—even before its first prompt. Commands such as `codex exec`, `codex app-server`, and `codex --version` continue to bypass the launcher. See [`docs/codex-live-delivery.md`](docs/codex-live-delivery.md) for the delivery and trust model. ## What you get Reads are MCP **resources**; writes are MCP **tools**. Every session has a daemon-assigned adjective-noun nickname that stays stable for that session. Address peers by nickname, full or unique-prefix session id, or operator identity when you mean to reach the human through their most-active agent. **Resources** (`resources/read`): | Resource | Contents | |---|---| | `marshal://whoami` | This session's `{ session_id, pid, cwd, operator, host }`. | | `marshal://roster` | Every live session: cwd, git branch, current task, last activity, room membership. | | `marshal://rooms` | Every room and its members. | | `marshal://messages` | Message history. Query params: `inbox`, `sent`, `unread`, `room`, `from`, `to_session`, `since`, `limit`. | **Tools** (`tools/call`): | Tool | Effect | |---|---| | `send_message(to, body)` | Interrupt one peer by nickname, session id/prefix, or operator identity. | | `broadcast(to_room, body)` | Ambient room update; an `@mention` also directly interrupts that peer. | | `join_room(room)` / `leave_room(room)` | Create/join or leave an ad-hoc room. | | `set_status(text)` | Update this session's free-form status (the `current_task` field on the roster). | | `ack_messages(message_ids)` | Mark message ids read for this session. | A peer's `send_message` is persisted first, then Marshal attempts a synchronous live-channel push when the recipient supports one. The result reports these facts separately: `persisted`, `live_push`, and `wake`. Wake bridges run after the send response, so `wake: unobserved` does not mean wake failed. Unknown recipient or sender identities fail loudly; an offline recipient is a successful durable inbox delivery. Direct messages start or enter a recipient turn, so they consume transcript context. Batch related details and reserve direct delivery for action, a blocker, or a needed reply. Use an unmentioned room broadcast for FYI/progress. Automatic body previews are bounded (2,000 characters per message and an 8,000-character hook-batch budget); the complete durable message remains available through `marshal://messages`. Codex wakes coalesce for 30 seconds so a burst can join the active turn. ## Architecture ``` Claude Code ── marshal-shim (stdio MCP) ──────┐ Codex ──────── marshal-shim hooks/bridge ─────┤ opencode ───── marshal-opencode plugin ───────┼── ws://localhost:6155 Pi ─────────── marshal-pi extension ──────────┤ │ Prime Agent ── marshal-prime-agent extension ─┘ └── marshal-daemon (roster + event log) ``` - **`marshal-daemon`** owns the live roster and the event log under `~/.local/state/marshal/events.jsonl`. One daemon serves every session that points its shim at it (local or remote). Run it under your favorite supervisor (or just `marshal-daemon &` in a terminal). - **`marshal-shim`** is the per-session stdio MCP server Claude Code spawns. It announces the session to the daemon on connect, watches for inbound messages, and forwards them onto stdout as channel notifications. - **`marshal-pi`** is the in-process Pi extension that registers sessions, exposes native tools, and injects direct messages as steering input. - **`marshal-prime-agent`** is the in-process Prime Agent extension that registers conversations, exposes tools, and injects live direct messages. - **`marshal-opencode`** provides the equivalent in-process opencode integration. - **`marshal-tui`** (optional) is a live ratatui dashboard of the roster + recent messages. ## Configuring the daemon address Marshal clients default to `ws://localhost:6155`. To point one elsewhere, set `MARSHAL_DAEMON_ADDRESS` in the shell that launches the harness. For Claude Code, you can instead pin it in a per-project `.mcp.json`: ```json { "mcpServers": { "marshal": { "env": { "MARSHAL_DAEMON_ADDRESS": "ws://10.0.0.5:6155" } } } } ``` The daemon's bind address is set with `MARSHAL_BIND` (default `0.0.0.0:6155` — set `MARSHAL_BIND=127.0.0.1:6155` to restrict to localhost). ## Workspace layout | Component | Description | |---|---| | [`marshal-entities`](crates/entities/) | Shared entity types and the `SendMessage` server command. | | [`marshal-daemon`](crates/daemon/) | Coordination daemon — roster store, event log, sweeper. | | [`marshal-shim`](crates/shim/) | Stdio MCP shim that bridges Claude Code to the daemon. | | [`marshal-tui`](crates/tui/) | Live terminal dashboard. | | [`marshal-ui`](crates/ui/) | Leptos operator dashboard (build target only — not published). | | [`@agent-marshal/marshal-pi`](plugins/marshal-pi/) | Pi native session extension. | | [`@agent-marshal/marshal-prime-agent`](plugins/marshal-prime-agent/) | Prime Agent live-conversation extension. | | [`marshal-opencode`](plugins/marshal-opencode/) | opencode native plugin. | Releases are driven by [`cargo-flux`](https://github.com/ignition-is-go/cargo-flux): conventional-commit `feat:` / `fix:` / breaking changes on `main` cut a stable release; the same on `dev` cuts a prerelease. See `flux.toml` and `.github/workflows/release.yml`. ## License MIT OR Apache-2.0 --- slug: /marshal/plugins/marshal-pi/readme title: Marshal for Pi source: marshal --- # Marshal for Pi [`@agent-marshal/marshal-pi`](https://www.npmjs.com/package/@agent-marshal/marshal-pi) is the native [Pi coding agent](https://pi.dev) bridge for [Marshal](https://github.com/ignition-is-go/marshal). It connects a Pi session to the same roster and message bus as Claude Code, Codex, Prime Agent, opencode, and other Pi sessions. The extension runs in-process. It: - registers the Pi session on the Marshal roster; - exposes native `marshal_*` tools without requiring the model to supply its own session ID; - subscribes to Marshal's live notification stream; and - injects direct messages with Pi's steering API, starting a turn when the recipient is idle. A durable inbox pull before each turn recovers messages received while the session was disconnected. Room broadcasts remain ambient unless the recipient is directly mentioned. ## Install Start a reachable Marshal daemon, then install the Pi package: ```bash cargo install marshal-daemon marshal-daemon & pi install npm:@agent-marshal/marshal-pi ``` Restart Pi after installation. No MCP server or wrapper is required. The extension uses `ws://localhost:6155` by default; point it at a remote daemon with: ```bash export MARSHAL_DAEMON_ADDRESS=ws://marshal.example.net:6155 pi ``` `MYKO_ADDRESS` is accepted as a fallback. `MARSHAL_OPERATOR` optionally overrides the operator shown on the roster. ## Verify Run `/marshal-status` in Pi or call `marshal_whoami`. Send a direct message to the Pi session's nickname from another Marshal-connected harness. The message appears in the transcript as: ```text new message from : ``` Peer content is untrusted input. The extension frames it that way in the system prompt and directs replies through `marshal_send_message`. ## Tools - `marshal_whoami` - `marshal_roster` - `marshal_messages` - `marshal_send_message` - `marshal_broadcast` - `marshal_join_room` - `marshal_leave_room` - `marshal_set_status` - `marshal_ack` ## Development From the repository root: ```bash cd plugins/marshal-pi npm ci --ignore-scripts --workspaces=false npm run gen npm run check cd ../.. pi --extension ./plugins/marshal-pi/src/index.ts ``` The generated TypeScript wire types come from the Rust `marshal-entities` crate and are produced before validation and publication. --- slug: /marshal/plugins/marshal-prime-agent/readme title: Marshal for Prime Agent source: marshal --- # Marshal for Prime Agent `@agent-marshal/marshal-prime-agent` is the native Prime Agent bridge for [marshal](https://github.com/ignition-is-go/marshal). It is separate from the `marshal-pi` package and targets Prime Agent's conversation lifecycle and canonical session identities. The extension runs inside each top-level Prime Agent conversation. Prime Agent RLM sub-agents stay part of their parent's Marshal session and are not added to the roster independently. It: - registers the top-level conversation on the Marshal roster using Prime Agent's session ID; - exposes native `marshal_*` tools to the model; - subscribes to Marshal's live notification stream; and - injects incoming direct messages with `pi.sendMessage(..., { deliverAs: "steer", triggerTurn: true })`. A direct message therefore appears in the transcript immediately and starts a turn when the recipient is idle. While the agent is working, it steers the active run at the next tool boundary. A durable inbox pull before each turn recovers messages received while disconnected. ## Install Start a reachable daemon, then install the Prime Agent package: ```bash cargo install marshal-daemon marshal-daemon & prime-agent package install npm:@agent-marshal/marshal-prime-agent ``` Restart Prime Agent after installation. No MCP server or wrapper is required. For a remote daemon: ```bash export MARSHAL_DAEMON_ADDRESS=ws://marshal.example.net:6155 prime-agent ``` The default is `ws://localhost:6155`. `MARSHAL_OPERATOR` optionally overrides the operator name shown on the roster. For development: ```bash cargo run -p marshal-entities --no-default-features --features codegen \ --bin typegen -- plugins/marshal-prime-agent/src/generated prime-agent --extension ./plugins/marshal-prime-agent/src/index.ts ``` ## Verify Run `/marshal-status` inside Prime Agent or use `marshal_whoami`. Send a direct message from another Marshal session to its nickname or session ID. It appears as: ```text new message from : ``` Peer content is untrusted input. The extension adds that warning to the system prompt and directs replies through `marshal_send_message`. Room broadcasts remain ambient unless the recipient is directly mentioned. ## Tools - `marshal_whoami` - `marshal_roster` - `marshal_messages` - `marshal_send_message` - `marshal_broadcast` - `marshal_join_room` - `marshal_leave_room` - `marshal_set_status` - `marshal_ack` --- slug: /marshal/docs/codex-live-delivery title: Codex live delivery source: marshal --- # Codex live delivery Codex command hooks can add context to a turn, but they cannot create a turn. That is why a direct Marshal message sent to an idle, normally launched Codex session remains unread until the user submits another prompt. Codex app-server provides the missing control plane. Multiple clients can attach to one app-server, and any client can issue `turn/start` for an idle thread. The original TUI receives that turn's normal streamed events. It also provides the earliest authoritative session lifecycle signal. `codex-run` connects the bridge first and waits until its app-server subscription is ready before launching the TUI. The TUI's `thread/started` notification contains the canonical root `sessionId` and cwd, so the bridge registers that session with Marshal immediately—without waiting for a first prompt or `SessionStart` hook. ## Delivery path ```text codex-run ──wait for subscriber──> local Codex bridge ▲ │ thread/started(sessionId, cwd) │ shared app-server <── attached TUI │ └── /hook/session-register ──> roster sender ──send_message──> marshal daemon ──durable unread Message │ ▼ local Codex bridge │ turn/start(thread_id) ▼ shared app-server ──> attached TUI │ ▼ UserPromptSubmit hook fetches, injects, then acknowledges inbox ``` The Marshal session id supplied to hooks is the Codex app-server thread id. That lets the bridge target the thread without guessing from cwd, process age, or rollout files. Eager registration uses the dedicated `/hook/session-register` endpoint. It only creates or refreshes the roster row; it never surfaces or acknowledges inbox messages because there is no model turn available to receive them. `SessionStart` remains the identity/context injection path and a fallback for Codex launches that do not use `codex-run`. The bridge deliberately does not mark the message read. The existing hook acknowledges it only after the daemon successfully writes the `` response back to Codex. If the app-server is unavailable, the thread is busy, or `turn/start` fails, the durable message stays unread and the ordinary prompt/tool-boundary path remains intact. Only unread direct messages are wake candidates. Room broadcasts are ambient, and the hook continues to frame peer content as untrusted coordination input: a peer cannot expand the operator's task or authority merely by waking a turn. ## Context and wake bounds Automatic delivery is intentionally smaller than durable history: - a live notification or single hook entry carries at most 2,000 body characters; - a hook surfaces at most 20 messages and divides an 8,000-character body budget across that batch; - truncated entries include their message id, and the complete body remains in `marshal://messages`; - successful wakes are coalesced per thread for 30 seconds, including after the first message is acknowledged, so a burst joins the active turn instead of creating one turn per message. On Linux and macOS, every interactive launcher keeps its lifecycle subscriber, but bridges attached to the same app-server and Marshal daemon elect one host-local wake leader with an advisory file lock. A new leader waits briefly before waking so the old leader's in-flight hook acknowledgement can settle. Dropping the leader process releases the lock and another bridge takes over. This prevents one unread row from producing duplicate turns when several Codex TUIs share the managed app-server. Native Windows launchers use isolated app-servers, so each bridge owns wake delivery for its own endpoint. Direct messages are therefore an interrupt and consume recipient context. Use them for an action, blocker, or needed reply and batch related information. Use a room broadcast without an `@mention` for FYI/progress; an `@mention` intentionally creates a direct interrupt. ## Send status model `send_message` returns as soon as the daemon has persisted the message and finished any synchronous live-channel push. A Codex bridge observes the unread message later, so the send response cannot claim whether `turn/start` succeeded: - `persisted: true` confirms durable message storage; - `live_push` is `delivered`, `unavailable`, `failed`, or `unknown`; - `wake` is `not_needed` after a delivered live push, otherwise `unobserved`. `unobserved` is deliberately not `failed`: a configured bridge may wake the recipient immediately after the send response, as happened in the normal idle Codex path. The legacy `delivery` and `delivered_live` fields remain for compatibility, but describe only the synchronous live push. ## Running it First install the normal Codex integration: ```sh marshal-shim codex-setup --daemon ws://marshal-host:6155 ``` Then use the live launcher for an interactive session: ```sh marshal-shim codex-run marshal-shim codex-run resume --last ``` On Linux and macOS, `codex-run` performs three actions: 1. idempotently starts `codex app-server daemon`; 2. starts `marshal-shim codex-bridge` and waits until its lifecycle subscription is ready; 3. runs `codex --remote unix:// ...`; the resulting `thread/started` event registers the session before any prompt is required. When the installed CLI is newer than the running managed app-server, `codex-run` does not attach a new session to the stale daemon. It starts an isolated loopback app-server from the installed binary for that launcher. The existing launchers remain attached to the old daemon, while every new local launcher gets the new Codex version immediately. The isolated server exits with its TUI. Once the managed daemon reaches the CLI version, new launchers share it again. This generation split applies to local `codex-run` sessions. Codex Remote Control exposes one managed server identity per `CODEX_HOME`; Codex must roll that server through its managed daemon lifecycle. The wrapper is a Marshal integration boundary, not a replacement Codex client. Marshal needs a second app-server connection because Codex hooks only run at turn boundaries and therefore cannot wake an idle TUI. The TUI remains the normal Codex executable and owns all user interaction and thread state. The per-TUI proxy records the selected `thread.id` from an explicit `thread/resume` request or the response to that TUI's `thread/start` request. It does not claim a thread from a shared `thread/started` notification, because another TUI on the same app-server can produce that notification. If the TUI then loses its transport while the lifecycle connection reports an app-server disconnect and a successfully initialized replacement connection, the launcher starts a fresh TUI process with `resume `. It preserves invocation-level model, profile, permission, working-directory, and display flags, but never replays positional prompts or image inputs. Normal user exits and non-transport failures are not restarted. An explicit UUID passed to `codex resume` pins the launcher to that thread immediately. A new or picker-selected session learns the id from its own proxied request and response, then keeps it across later server generations. The bridge uses the same ownership proof to add the Marshal nickname to the Codex thread name. It waits for a non-empty Codex name, then writes `[] ` through `thread/name/set`. A `thread/name/updated` event applies the rule again after an automatic title or a user rename. The marker is parseable, so resumes, bridge restarts, and repeated events do not add duplicate prefixes. A bridge that has no per-TUI ownership proof continues registration and wake delivery but does not rename threads. This follows the app-server connection lifecycle rather than attempting to preserve a dead socket: every replacement connection performs `initialize` / `initialized`, and the fresh TUI resumes the persisted thread. The bridge stays alive across the TUI replacement and continues its normal registration and wake duties. App-server and bridge startup are bounded. If either cannot become ready, `codex-run` emits a warning and immediately launches the same Codex binary with the original arguments in native mode. Live wake is unavailable for that run, but an integration failure never prevents the operator from reaching Codex; the durable hook inbox remains available at the next turn boundary. Codex does not provide that managed daemon on native Windows. There, `codex-run` instead starts one `codex app-server` child on an ephemeral `127.0.0.1` WebSocket port, attaches the bridge and TUI to it, then terminates the child when the TUI exits. The bridge refuses non-loopback WebSocket endpoints; no unauthenticated app-server listener is exposed to the network. The bridge normally exits with that TUI. During a managed Unix app-server replacement it remains alive long enough to reconnect and supervise the TUI resume. More than one live launcher may run on a host; Unix bridges elect one wake leader while all of them continue lifecycle registration. Windows launchers use separate ephemeral ports. For diagnostics or a supervisor-managed deployment, run the bridge directly: ```sh marshal-shim codex-bridge \ --daemon ws://marshal-host:6155 \ --socket "$CODEX_HOME/app-server-control/app-server-control.sock" # Native Windows or a manually supervised local app-server: marshal-shim codex-bridge \ --daemon ws://marshal-host:6155 \ --endpoint ws://127.0.0.1:4500 ``` ## Making it the fleet default Codex currently exposes the shared endpoint as a CLI flag, not a `config.toml` setting. Make the behavior default at the agent-launcher layer: - interactive `codex` and `codex resume` jobs invoke `marshal-shim codex-run ...`; - non-interactive and administrative subcommands (`codex exec`, `codex mcp`, `codex update`, and similar) continue to invoke the real Codex executable; - the Ansible role runs `marshal-shim codex-setup` so SessionStart, SessionEnd, prompt, and tool hooks stay installed and trusted. Do not overwrite Codex's own managed executable or symlink. Its updater owns that path. A fleet launcher or explicit wrapper command is stable across Codex updates and avoids recursion when `codex-run` delegates back to the real binary. ## Platform boundary The managed app-server daemon and its Unix-domain control socket are Unix-only. Native Windows uses Codex's loopback WebSocket transport under the launcher-owned process lifecycle described above. It is intentionally not a machine-wide daemon: loopback keeps the endpoint private to the host, while per-launcher processes avoid port sharing and make cleanup deterministic. --- slug: /levi/readme title: Levi source: levi --- # levi Git-aware, agent-first, distributed issue tracker. Task status is a function of git history, storage travels with the repo (`refs/levi/events`), and the primary consumer is a coding agent asking "what should I work on next?" A task closed at commit X is closed only where X is an ancestor of HEAD: feature-branch work shows as done on that branch and still-open on main until merged. Fully functional offline from a bare clone — no server, no daemon. Design: `docs/superpowers/specs/2026-07-18-levi-design.md`. ## Build ```sh cargo build --release # levi + levi-hub (native) cd levi-dash && trunk build # dashboard (wasm; needs trunk + wasm32 target) ``` ## Quickstart ```sh cd your-repo levi init [--hub host:port] # adopt-or-create the project + agent # instructions (AGENTS.md / CLAUDE.md) # + hub in .levi/config.toml levi add "fix flux capacitor" -p p0 -l engine levi add "polish docs" --dep lv-3f2a # blocked by the first task levi next # highest-priority eligible work + why levi next --claim # …and atomically claim it git commit -am "the fix" levi close lv-3f2a # anchored at HEAD: closed only where # this commit is in ancestry levi ls --json # stable schemas on every read command levi show lv-3f2a # detail: deps, claim, comments, history levi check-claims --git-ref feature # CI: fail if its claimed tasks are open at HEAD ``` Anchoring: `levi close ID` anchors at HEAD; `--anchor SHA` overrides; `--no-anchor` closes everywhere (tasks unrelated to code state). `reopen` works identically. Rebased-away anchors are detected and warned about. Claims are advisory, keyed to (developer, machine, worktree), newest wins, and expire after 24h (`[claim] ttl_secs` in config). New claims also record the current full branch ref and therefore require a symbolic (non-detached) HEAD. `levi start`/`steal`/`drop` manage live coordination; parallel `levi next --claim` on one machine never hands two agents the same task. Branch responsibility is durable even after a claim is dropped, expires, is stolen, or is automatically released by `levi close`. In CI, run `levi check-claims --git-ref "$BRANCH"`; it exits non-zero unless every task ever claimed by that branch is closed in the tested HEAD ancestry. Use `--at REV` to test another revision and `--json` for the versioned `levi.check-claims/1` result. Short branch names normalize to `refs/heads/…`. CI must fetch the custom event ref first, for example `git fetch origin +refs/levi/events:refs/levi/events` (or run `levi sync`). Ref identity is exact: renaming a branch does not transfer its old obligations. ## GitHub Action Levi publishes a colocated composite action backed by checksum-verified native release binaries for Linux x86-64, Windows x86-64, and macOS x86-64 or ARM64. It fetches `refs/levi/events` and runs the branch claim gate directly: ```yaml permissions: contents: read steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - id: levi uses: ignition-is-go/levi@v0.9.0 with: git-ref: ${{ github.head_ref || github.ref_name }} ``` `git-ref` selects the branch that owns the claims. `at` selects the tested Git history and defaults to `HEAD`, so pull-request merge commits and detached CI checkouts work correctly. `remote` defaults to `origin`; set `fetch-events` to `false` only when `refs/levi/events` is already present locally. The action exposes `version`, `path`, and the compact `levi.check-claims/1` JSON `result`. Set `check: false` for setup-only use; the installed `levi` binary is added to `PATH`. `version` defaults to the CLI version carried by the selected action release. Release archives and their SHA-256 sidecars are built and smoke-tested on every supported runner before publication completes. ## Sync ```sh levi sync # all legs; --no-git / --no-hub to skip one ``` - **git leg**: `refs/levi/events` is fetched from / pushed to your remote (`[sync] remote` in config, default `origin`). Histories union-merge; events are content-addressed and immutable, so sync never conflicts. - **hub leg**: with a hub address in `.levi/config.toml` (written by `levi init --hub `, committed with the repo), events are exchanged with a levi-hub — machines that share no git remote converge through it. Every mutating command also does an opportunistic best-effort hub sync (`--no-sync` to skip). - **facts leg**: commit-graph slices (sha → parents, branch heads) are published so the git-free hub can resolve ancestry. Config: repo-level `.levi/config.toml` overrides the user-global `~/.config/levi/config.toml` (`[hub] address`, `[sync] remote`, `[claim] ttl_secs`). ## Hub + dashboard ```sh MYKO_POSTGRES_URL=postgres://… levi-hub serve --bind 0.0.0.0:7377 ``` The hub is a plain myko CellServer (`ws:///myko`); without `MYKO_POSTGRES_URL` it holds events in memory only. `levi watch` streams live events from it. The dashboard is a standalone Leptos CSR app: ```sh cd levi-dash && trunk serve # dev, http://localhost:1420 cd levi-dash && trunk build # dist/ — host the static files anywhere ``` It connects to `ws://:7377/myko` by default; override with `?hub=host:port`. Built on [mullion] splittable panes (activity bar on each pane; split/drag/close like an IDE) and the [pulse-leptos-ui] design system. Views: overview (open/closed counts, P0 alerts, live activity feed), in-flight claims by dev → machine → worktree, and a project browser with a branch selector: any project's tasks as resolved against any branch, from commit facts alone. [mullion]: https://github.com/ignition-is-go/mullion [pulse-leptos-ui]: https://crates.io/crates/pulse-leptos-ui ## Testing `cargo test --workspace` covers the resolution fold and ranking (pure unit tests over synthetic DAGs), CLI integration against scripted multi-branch / merge / rebase / worktree repos, concurrent CAS appends and parallel claims, and two-repo + in-process-hub sync convergence. ## Cross-project File bugs upstream and block on them across repos — the hub relays: ```sh levi add --project myko "Partial serializes None as explicit null" -p p2 # → myko/lv-7c21 (hub-acknowledged; lands in myko's repo on their next sync) levi comment myko/lv-7c21 "repro from our side: ..." levi dep add lv-ee52 --on myko/lv-7c21 --via "cargo: crates.io myko ^4.24" ``` A cross-project blocker resolves against how you actually consume the project: a sibling checkout on this machine wins (exact, offline — levi auto-registers every checkout it runs in), otherwise the hub's view of their default branch (cached at sync time; `ls`/`next` never wait on the network), otherwise unknown-means-blocked. The `--via` note travels with the dep so the agent that gets unblocked knows how to verify the fix is actually reachable (published release, updated pin, …) before starting. ## License AGPL-3.0-or-later, matching the [myko] framework it builds on. [myko]: https://github.com/ignition-is-go/myko --- slug: /myko-mcp/overview title: Myko MCP source: myko-mcp --- # Myko MCP Myko MCP gives agents access to the operations registered by a Myko application. It connects an agent to application state and commands through the Model Context Protocol. The [Myko framework](/myko/readme) provides the underlying event-sourced state and reactive queries. ## Services These services expose their registered application operations through Myko MCP. Connect to the service you want to work with, then use `search` to discover its available operations. | Service | Application state and operations | | --- | --- | | [Rocketship](/rship/get-started/introduction) | Show-control state, scenes, targets, bindings, and executor connections. | | [Tactus](/tactus/readme) | Music analysis, playback, and analysis jobs. | | [Optik](/optik/readme) | Cameras, calibration, tracking, and video-processing configuration. | | [Phosphor](/phosphor/readme) | Media-renderer state and controls. | | [Pulse Cluster](/pulse-cluster/overview) | Render-fleet recipes, lifecycle operations, and observations. | | [Pulse Agents](/pulse-agents/overview) | Remote agent-session control-plane state and operations. | | [Marshal](/marshal/readme) | Agent presence, rooms, and messaging. | | [Levi hub](/levi/readme) | Shared task state. Levi's CLI remains the entry point for Git-aware repository workflows. | | [Program Compositor](https://github.com/ignition-is-go/program-compositor) | Program-compositor console state and controls. | | [Pulse Pixelstream](https://github.com/ignition-is-go/pulse-pixelstream) | Viewer presence and shared stream control. | | [Pulse Alerts](https://github.com/ignition-is-go/pulse-alerts) | Alert state and notification delivery. | | [NMOS node](https://github.com/ignition-is-go/nmos-rs/blob/main/nmos-node/README.md) | NMOS device and stream-routing state. | | [Rocketship Asset Store](https://github.com/ignition-is-go/rship/blob/dev/apps/asset_store/README.md) | Asset metadata and storage operations. | The operations visible to a client depend on the service version and configured filters. Use the endpoint provided for your environment. [Tachyon](https://github.com/ignition-is-go/tachyon) also provides a scoped MCP gateway in front of a consuming application's Myko endpoint for admitted Ansible runs. ## Its role in agent tooling The agent tools answer different questions: - [Pulse Agents](/pulse-agents/overview) runs durable remote sessions and provides their Console, transcripts, and recovery. - [Marshal](/marshal/readme) tracks live agent sessions and delivers messages between them. It answers who is available and how to reach them. - [Levi](/levi/readme) tracks tasks, dependencies, and claims. Task completion follows Git ancestry, so it answers what work is done on a particular checkout. - Myko MCP exposes application operations. It answers what an agent can read or change in the connected service. A service can use Myko MCP alongside Marshal and Levi. Each tool keeps its own responsibility: session coordination, task tracking, or application access. ## Discover and execute operations Myko MCP exposes `search`, `execute`, and `connection_status` tools. `search` returns registered queries, views, reports, and commands. Detailed results include argument metadata and validated examples when available. `execute` runs a JavaScript async function body against the generated `myko.*` API. A script can combine calls and return a calculated result in one tool call. Each operation is checked against visibility filters, argument constraints, and its compiled deserializer before dispatch. The application server hosts the endpoint at `/myko/mcp` over HTTP POST, WebSocket, and server-sent events. A stdio integration is also available for clients that require it. See the [Myko MCP reference](/myko/readme#mcp--model-context-protocol) for connection options, discovery examples, and per-client filters. --- slug: /rocketship-sdks/overview title: Overview source: rocketship-sdks --- # Rocketship SDKs Rocketship SDKs connect applications and integrations to Rocketship. Executor integrations register instances and targets, expose actions, and publish emitters. Some SDKs also provide application capabilities beyond executor registration. ## Choose an SDK The Rocketship repository contains these language implementations under `libs/sdk/`: | SDK | Source directory | | --- | --- | | [Rust](rust.md) | `rs` | | [TypeScript](typescript.md) | `ts` | | [Python](python.md) | `python` | | [C++](cpp.md) | `cpp` | | [C#](csharp.md) | `csharp/RshipSdk` | | [Embedded C](embedded.md) | `embedded` | | [Swift](swift.md) | `swift` | The [SDK source tree](https://github.com/ignition-is-go/rship/tree/dev/libs/sdk) is part of the Rocketship repository, which tracks `dev`. These pages identify the implementation and its reference material. They do not certify release readiness or equal feature support across languages. ## Application SDKs These integrations provide Rocketship's SDK capabilities inside host applications: | SDK | Host application | Source | | --- | --- | --- | | [RshipExec](/pulse-unreal/plugins/rshipexec/readme) | Unreal Engine | [pulse-unreal](https://github.com/ignition-is-go/pulse-unreal/tree/main/Plugins/RshipExec) | | [Rship TouchDesigner](/rship-touchdesigner/readme) | TouchDesigner and Notch | [rship-touchdesigner](https://github.com/ignition-is-go/rship-touchdesigner) | Use these alongside the language SDKs when building application-specific integrations. ## Build an integration Read [Executor development](/rship/executor-development/overview) for the integration model, then [Connect executors](/rship/executor-development/connect-executors) for connection concepts. Use the selected SDK's current examples for its actual API. Use [Rocketship's data model](/rship/core-concepts/data-model) for entities and relationships. Framework authors can also use [Myko](/myko/readme) for application state and [Hyphae](/hyphae/hyphae/readme) for reactive dataflow. ## Verify against the selected revision Keep the SDK and server contracts aligned. Follow the language implementation's build and test instructions. Check the [native required-view migration](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/docs/required-views-migration.md) when working on executor state, properties, or composition engines. --- slug: /rocketship-sdks/rust title: Rust source: rocketship-sdks --- # Rust SDK The Rust SDK is the `rship-sdk` crate. `SdkClient` is its application entry point. Executor capabilities live under `rship_sdk::executors`; asset capabilities live under `rship_sdk::assets`. ## Source and reference - [SDK source](https://github.com/ignition-is-go/rship/tree/dev/libs/sdk/rs) - [Reference entry point](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/rs/README.md) - [Runnable examples and verification](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/rs/examples/README.md) ## Integration context Start with the [SDK overview](overview.md) and [executor integration concepts](/rship/executor-development/overview). Check the SDK at the same Rocketship revision as your application and server. The language implementations have separate examples and do not imply feature parity. --- slug: /rocketship-sdks/typescript title: TypeScript source: rocketship-sdks --- # TypeScript SDK The TypeScript SDK is the `@rship/sdk` workspace package. Its source entry point is `src/index.ts`. The runnable example uses `RshipExecClient` to register an instance, targets, actions, and emitters. ## Source and reference - [SDK source](https://github.com/ignition-is-go/rship/tree/dev/libs/sdk/ts) - [Reference entry point](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/ts/package.json) - [Runnable TypeScript example](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/ts/examples/hello_rship.ts) ## Integration context Start with the [SDK overview](overview.md) and [executor integration concepts](/rship/executor-development/overview). Check the SDK at the same Rocketship revision as your application and server. The language implementations have separate examples and do not imply feature parity. --- slug: /rocketship-sdks/python title: Python source: rocketship-sdks --- # Python SDK The Python SDK is the `rship-sdk` workspace package. Its package metadata requires Python 3.11 or later and depends on Myko. Its README records model and serialization conventions. ## Source and reference - [SDK source](https://github.com/ignition-is-go/rship/tree/dev/libs/sdk/python) - [Reference entry point](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/python/README.md) ## Integration context Start with the [SDK overview](overview.md) and [executor integration concepts](/rship/executor-development/overview). Check the SDK at the same Rocketship revision as your application and server. The language implementations have separate examples and do not imply feature parity. --- slug: /rocketship-sdks/cpp title: C++ source: rocketship-sdks --- # C++ SDK The C++ SDK uses C++17 and CMake. Its README covers dependencies, builds, instance management, actions, and emitters. ## Source and reference - [SDK source](https://github.com/ignition-is-go/rship/tree/dev/libs/sdk/cpp) - [Reference entry point](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/cpp/README.md) ## Integration context Start with the [SDK overview](overview.md) and [executor integration concepts](/rship/executor-development/overview). Check the SDK at the same Rocketship revision as your application and server. The language implementations have separate examples and do not imply feature parity. --- slug: /rocketship-sdks/csharp title: C# source: rocketship-sdks --- # C# SDK The C# SDK is the `RshipSdk` .NET package. The current project targets .NET 8. Its README covers client setup, instances, targets, action handlers, and emitters. ## Source and reference - [SDK source](https://github.com/ignition-is-go/rship/tree/dev/libs/sdk/csharp/RshipSdk) - [Reference entry point](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/csharp/RshipSdk/README.md) - [Project and target framework](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/csharp/RshipSdk/RshipSdk.csproj) ## Integration context Start with the [SDK overview](overview.md) and [executor integration concepts](/rship/executor-development/overview). Check the SDK at the same Rocketship revision as your application and server. The language implementations have separate examples and do not imply feature parity. --- slug: /rocketship-sdks/embedded title: Embedded C source: rocketship-sdks --- # Embedded C SDK The embedded SDK provides hardware profiles for constrained microcontrollers, RTOS devices, and embedded Linux. The profiles use different transports and resource budgets. Select a profile using the source documentation. ## Source and reference - [SDK source](https://github.com/ignition-is-go/rship/tree/dev/libs/sdk/embedded) - [Reference entry point](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/embedded/README.md) ## Integration context Start with the [SDK overview](overview.md) and [executor integration concepts](/rship/executor-development/overview). Check the SDK at the same Rocketship revision as your application and server. The language implementations have separate examples and do not imply feature parity. --- slug: /rocketship-sdks/swift title: Swift source: rocketship-sdks --- # Swift SDK The Swift implementation is a Swift package named `RshipSDK`. Its package manifest declares Swift tools 5.9 and a dependency on Starscream. The package has no README in its SDK directory; use its manifest and source as the entry points. ## Source and reference - [SDK source](https://github.com/ignition-is-go/rship/tree/dev/libs/sdk/swift) - [Reference entry point](https://github.com/ignition-is-go/rship/blob/dev/libs/sdk/swift/Package.swift) ## Integration context Start with the [SDK overview](overview.md) and [executor integration concepts](/rship/executor-development/overview). Check the SDK at the same Rocketship revision as your application and server. The language implementations have separate examples and do not imply feature parity. --- slug: /myko/readme title: myko source: myko --- # Myko **Event-sourcing CQRS framework with reactive queries.** Define entities once in Rust; get commands, events, queries, real-time subscriptions, and cross-language bindings automatically. License: MIT OR Apache-2.0 · Workspace version: see `Cargo.toml` --- ## Core concept ``` Command ──▶ Handler ──▶ Event ──▶ Store ──▶ Reactive Query ──▶ Client │ │ │ └──▶ Saga ──▶ Command └── validation ``` - **Commands** mutate state. Synchronous validation, then events. - **Events** are persisted (Postgres) and replicated. - **Stores** hold current state, derived from events. - **Queries** and **reports** are reactive views of stores. - **Sagas** subscribe to events and emit follow-up commands. - **Clients** subscribe to queries/reports over WebSocket and receive live diffs. The framework handles persistence, real-time sync, federation between server peers, and type generation across language bindings. --- ## Workspace layout ``` libs/ ├── myko/ │ ├── core/ Canonical Rust framework (entities, queries, commands, sagas, stores) │ ├── macros/ #[myko_item] proc macro + relationship attributes │ ├── server/ Tokio-based server runtime: WS gateway, Postgres backend, │ │ peer federation, MCP endpoint │ ├── leptos/ Leptos integration (web UI) │ ├── ts/ TypeScript port + generated bindings │ ├── py/, python/ Python bindings │ ├── cpp/ C++ bindings │ ├── csharp/ C# bindings │ ├── ui-svelte/ Svelte UI integration │ ├── ui-vue/ Vue UI integration │ └── debug/ Diagnostic tooling └── autosocket/ Auto-reconnecting WebSocket transport (native + WASM) docs/superpowers/specs/ Design specs for in-flight features ``` **External dependency:** [hyphae](https://github.com/ignition-is-go/hyphae) (reactive dataflow). Consumed from crates.io (`^0.5.1`). --- ## Quick start Prerequisites: **Rust** (edition 2024), **Bun** (1.3+), **Postgres** if running the server with durability. ```bash # Install JS deps (used for type generation, formatting, UI bindings) bun install # Type-check the whole workspace cargo flux run check # Run all tests cargo flux run test # Lint (cargo clippy + biome) cargo flux run lint # Regenerate TS bindings from Rust types cargo flux run gen ``` [`cargo flux`](https://github.com/ignition-is-go/cargo-flux) orchestrates the polyglot workspace. Tasks are defined in `flux.toml`. --- ## Defining entities ```rust use myko_macros::myko_item; #[myko_item] pub struct Target { pub name: String, } ``` Auto-generated for every `myko_item`: | Item | Purpose | | ------------------- | -------------------------------------- | | `id: Arc` | Stable entity identity (UUID) | | `hash: Arc` | Content hash for change detection | | `GetAllTargets` | Query — every target | | `GetTargetsByIds` | Query — by id set | | `GetTargetsByQuery` | Query — filtered | | `GetTargetById` | Query — single | | `CountAllTargets` | Report — count | | `DeleteTarget` | Command — delete one | | `DeleteTargets` | Command — delete many | | `TargetQuery` | Filter type for query parameters | ### Relationship attributes ```rust #[myko_item] pub struct BindingNode { #[belongs_to(Scene)] // cascade delete: parent DEL → child DEL pub scene_id: Arc, #[owns_many(BindingEdge)] // parent DEL → children DEL; child DEL → parent UPDATE pub edges: Vec>, #[ensure_for(Project)] // auto-create one per Project pub project_id: Arc, #[searchable] // index for full-text search pub label: String, #[myko_client_id] // auto-populate with connecting WS client id pub created_by: Arc, } ``` --- ## Running a server ```rust use myko_server::{MykoServer, postgres::PostgresConfig}; #[tokio::main] async fn main() -> Result<(), Box> { let _telemetry = myko_server::telemetry::init_from_env(); let server = MykoServer::builder() .with_bind_addr("0.0.0.0:5155".parse()?) .with_postgres(PostgresConfig::from_env()?) // durable event log + LISTEN/NOTIFY .build(); server.run().await } ``` On startup the server logs: ``` Myko gateway: ws://0.0.0.0:5155/myko | MCP: /myko/mcp (POST + WS + SSE) ``` ### Endpoints | Path | Method | Purpose | | ----------- | ----------------------------------- | --------------------------------------------- | | `/myko` | `GET` + WS upgrade | Main Myko gateway (clients, federation peers) | | `/myko/mcp` | `POST` | MCP JSON-RPC (Streamable HTTP) | | `/myko/mcp` | `GET` + WS upgrade | MCP over WebSocket (subprotocol `mcp`) | | `/myko/mcp` | `GET` + `Accept: text/event-stream` | MCP SSE stream (keepalive in v1) | All endpoints share a single TCP listener; the front-door router peeks at the HTTP request line + headers and dispatches. ### Environment | Variable | Default | Description | | -------------------------------- | --------------------- | ------------------------------------------ | | `MYKO_ADDRESS` | `ws://localhost:5155` | Used by the stdio MCP binary and clients | | `MYKO_POSTGRES_URL` | — | Postgres connection string | | `MYKO_POSTGRES_CATCH_UP_TIMEOUT_SECS` | `300` | Maximum startup snapshot/catch-up wait before the server exits; `0` disables the limit | | `MYKO_PORT` | `5155` | Server bind port (when wired through env) | | `MYKO_TRACING_ENDPOINT` | — | Fallback OTLP/HTTP base endpoint for logs, traces, and metrics (see `myko_server::telemetry::init_from_env`); unset = local console logging only | | `MYKO_CCMD_MONITOR` | `0` | Set `1` to log command timing | | `MYKO_CCMD_TIMEOUT_MS` | — | Slow-command threshold for warn logs | | `MYKO_MEM_PROFILE_INTERVAL_SECS` | `60` | Metrics export interval (seconds); applies when OTLP export is configured | | `MYKO_MALLOC_TRIM_INTERVAL_SECS` | — | Periodic `malloc_trim(0)` probe: logs RSS before/after returning free glibc arena pages (distinguishes allocator page retention from real retention). Unset/0 = off; glibc-only. Measures glibc arenas only — meaningless if the host binary sets a non-glibc `#[global_allocator]` (e.g. jemalloc); use that allocator's own stats instead | --- ## Cross-language bindings > **Rule:** Logic lives in Rust. Cross-language types are *generated*, not duplicated by hand. If a binding needs a type, add it to the Rust side and re-run `cargo flux run gen`. | Language | Path | Notes | | ---------- | -------------------------- | ----------------------------------------- | | TypeScript | `libs/myko/ts/` | Generated by `cargo flux run gen` (ts-rs) | | Python | `libs/myko/py/`, `python/` | pyproject-based | | C++ | `libs/myko/cpp/` | | | C# | `libs/myko/csharp/` | | | Leptos | `libs/myko/leptos/` | Rust web client integration | | Svelte | `libs/myko/ui-svelte/` | Wraps the TS client | | Vue | `libs/myko/ui-vue/` | Wraps the TS client | --- ## Development workflow ### Iteration loop ```bash # Fast check during edits (preferred over cargo build) cargo check # Tests cargo test -p myko-server # Single test cargo test -p myko my_test_name -- --exact --nocapture # Lint as CI would (strict) cargo clippy --all-targets --all-features -- -D warnings # Format cargo fmt --all # Rust bun run format:all # JS/TS via Prettier ``` Respect the checkout’s configured Cargo target directory. Managed workspaces isolate build output without per-agent overrides. ### Bacon (background type checker) `bacon.toml` is configured. `.bacon-locations` is updated by bacon — **check it before running clippy/check** to fix errors in order, since later errors often resolve when the first is fixed. ### Hot reload Assume the user is running entities and codegen in hot-reload mode. **Do not** start dev servers, run `cargo flux run gen`, or kick off long-running tasks yourself unless explicitly asked. ### Type generation Whenever a type that crosses the Rust↔TS boundary changes, run: ```bash cargo flux run gen ``` This regenerates `libs/myko/ts/src/generated/`. Never hand-edit generated files. ### Conventional commits ``` feat(scope): add new feature fix(scope): fix a bug chore(scope): tooling, dependencies, release plumbing docs(scope): documentation only refactor(scope): no behavior change perf(scope): performance work test(scope): tests only ``` Scopes match crate or area: `myko`, `myko-server`, `mcp`, `autosocket`, `ts`, `leptos`, `ws`, etc. ### Release Automated. Pushing to `main` or `dev` stamps a version, tags, creates a GitHub Release, and publishes to crates.io and npm. Canary versions ship from `dev`, stable from `main`. --- ## Performance See [`libs/myko/core/OPTIMIZATION.md`](libs/myko/core/OPTIMIZATION.md) for strategies, benchmarks, and the rationale behind the cell-based hot path. Diagnostic features (memory profiles, ingest stats, command timing) are opt-in via env vars and have near-zero overhead when disabled. --- ## MCP — Model Context Protocol Myko ships an MCP endpoint so AI agents can call your queries / reports / commands as tools. Two modes: 1. **In-server HTTP/WS/SSE** at `/myko/mcp` on the same port as the Myko gateway. Auto-discovered tools, per-client filtering via headers. See [Endpoints](#endpoints) above. 2. **Stdio binary** (`myko::mcp::McpServer::run_stdio`) for editor integrations that prefer stdio MCP. Connects to a running Myko server via `MYKO_ADDRESS`. ### Discover and execute The MCP tools are `search`, `execute`, and `connection_status`. `search` returns compact entries for registered queries, views, reports, and commands. Request `{"query": "GetAllServers", "kind": "query", "details": true}` to include wire argument names, aliases, Rust/TypeScript types, defaulted fields, the exact result wrapper, and a small validated example. `execute` accepts a JavaScript async function body: ```javascript const servers = await myko.query('GetAllServers', {}) return { count: servers.items.length } ``` Query/view results contain `items` and `count`; report/command payloads are in `result`. Use `Promise.all` for independent calls and JavaScript collections to join, deduplicate, filter, and aggregate their payloads. Return the calculated answer so raw data and arithmetic do not need another model round trip. Every `myko.*` call inside a script checks visibility, argument constraints, and the operation's compiled Serde deserializer before dispatch. Diagnostics identify the operation and failing field path, including nested fields. Errors can be caught within the script, so one rejected call need not discard the other calls in a batch. Wire metadata comes from Serde's attribute parser, and wrapper descriptions share their definition with result serialization. Primitive/container inputs get examples automatically. For custom types, the application can register example arguments without adding domain concepts to Myko: ```rust myko::register_operation_example!("command", MyCommand, serde_json::json!({ "input": { "mode": "example" } })); ``` Examples are checked against the compiled deserializer before discovery returns them. If no valid example can be constructed, detailed discovery says so rather than returning an invalid call. Types remain nominal: discovery does not expand custom DTOs into a complete JSON Schema or run a TypeScript compiler. Schema resources remain available at `myko://schema//`; use detailed `search` for the execution contract and examples. ### Per-client filtering Lock down what an MCP-client config can call without trusting the client itself. Two filter layers, both **client-configured**, composed AND. Both follow an Allow/Deny header pair with the same precedence (deny wins). **1. Tool visibility** — glob allow/deny over tool names. A hidden tool is omitted from `tools/list` and a `tools/call` against it returns the MCP **Protocol Error** (`-32602`, `"Unknown tool: …"`) — indistinguishable from a tool that doesn't exist. ``` X-Myko-Tool-Visibility-Allow: query:*,report:* X-Myko-Tool-Visibility-Deny: command:Delete* ``` Patterns: `*`, `prefix*`, `*suffix`, exact. **2. Tool callability** — per-tool, per-arg JSON value lists. Failure surfaces as an MCP **Tool Execution Error** (`isError: true` content) — the spec's "Invalid input data" category, distinct from a Protocol Error. ``` X-Myko-Tool-Callable-Allow: {"command:RunPlaybook":{"playbook_id":["site","deploy"]}} X-Myko-Tool-Callable-Deny: {"command:Tag":{"namespace":["prod"]}} ``` JSON shape per header: `{ "": { "": [values] } }`. Allow is positive (the arg must be present and its value must be in the list). Deny excludes (the arg's value must not be in the list). **Stdio transport** has no headers; the same four knobs come from env vars: - `MYKO_MCP_TOOL_VISIBILITY_ALLOW` - `MYKO_MCP_TOOL_VISIBILITY_DENY` - `MYKO_MCP_TOOL_CALLABLE_ALLOW` (JSON) - `MYKO_MCP_TOOL_CALLABLE_DENY` (JSON) Visibility applies to `tools/list`, `tools/call`, `resources/list`, `resources/read`. Callability applies only to `tools/call`. ### Connecting a client **curl sanity check:** ```bash curl -sS -X POST http://localhost:5155/myko/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq ``` **Claude Code (project `.mcp.json`)** — name filter only: ```json { "mcpServers": { "myko": { "type": "http", "url": "http://localhost:5155/myko/mcp", "headers": { "X-Myko-Tool-Visibility-Allow": "query:*,report:*" } } } } ``` **With argument allowlist** (e.g. only let the agent run two specific playbooks): ```json { "mcpServers": { "myko-restricted": { "type": "http", "url": "http://localhost:5155/myko/mcp", "headers": { "X-Myko-Tool-Visibility-Allow": "query:*,report:*,command:RunPlaybook", "X-Myko-Tool-Callable-Allow": "{\"command:RunPlaybook\":{\"playbook_id\":[\"site\",\"deploy\"]}}" } } } } ``` **Claude Desktop / Inspector:** point them at the same URL with the same headers. Behind a TLS-terminating reverse proxy the public URL is `https:///myko/mcp`. --- ## AI agents This repo is set up for AI-assisted development with both [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) and other agents. - [`CLAUDE.md`](CLAUDE.md) — project-specific instructions Claude Code loads automatically. - [`AGENTS.md`](AGENTS.md) — generic agent guidance (toolchain commands, formatting, conventions). ### The short version for agents 1. **Rust first.** New logic goes in Rust unless you're explicitly working on a legacy TS path. Generate bindings; never duplicate types by hand. 2. **`cargo check`, not `cargo build`.** Respect the checkout’s configured target directory. 3. **Check `.bacon-locations`** before running clippy/check. Fix errors in order. 4. **Don't run `cargo flux run gen` or start dev servers yourself** — assume the user has hot reload running. 5. **No hardcoded field/type name strings.** Use macros and type constructors. 6. **Use real entities in tests.** Don't construct JSON manually. 7. **Comments explain *why*, not *what*.** Initials and tags: `// TODO(ts): ...`, `// NOTE(ts): ...`. 8. **Conventional commits** with scope; never bundle unrelated changes. ### Specs and design docs Every non-trivial change starts with a spec under `docs/superpowers/specs/--design.md`. The spec is the source of truth; implementation plans and PRs reference it. --- ## Architecture references | Topic | Where | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Optimization & benchmarks | [`libs/myko/core/OPTIMIZATION.md`](libs/myko/core/OPTIMIZATION.md) | | MCP endpoint spec | [`docs/superpowers/specs/2026-05-20-mcp-http-endpoint-design.md`](docs/superpowers/specs/2026-05-20-mcp-http-endpoint-design.md) | | Agent instructions | [`CLAUDE.md`](CLAUDE.md), [`AGENTS.md`](AGENTS.md) | --- ## License MIT OR Apache-2.0 for the framework crates (`myko`, `myko-macros`, `autosocket`). The server runtime (`myko-server`) is AGPL-3.0-or-later. See individual crate `Cargo.toml` for specifics. ### Shared application telemetry Rust applications and SDK hosts can depend on `myko-telemetry` without the server runtime. Initialize once before constructing clients, and hold the guard until process shutdown: ```rust,no_run let _telemetry = myko_telemetry::init_from_env(); ``` This installs console logging, bridges existing `log` macros into `tracing`, and optionally exports logs, traces, and metrics over OTLP HTTP/protobuf. Remove an earlier `env_logger::init()` call. Hosts with custom subscribers can compose `myko_telemetry::otel_layer_from_env()` instead; the server module re-exports both entry points. ```sh OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_SERVICE_NAME=phosphor OTEL_RESOURCE_ATTRIBUTES=host.name=phosphor-01,deployment.environment.name=development RUST_LOG=info,phosphor=debug,rship_sdk=debug ``` The base endpoint receives `/v1/logs`, `/v1/traces`, and `/v1/metrics` requests. Standard `OTEL_EXPORTER_OTLP_{LOGS,TRACES,METRICS}_ENDPOINT` overrides are complete per-signal URLs, including their path. They take precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`, which takes precedence over the legacy `MYKO_TRACING_ENDPOINT` base URL. Exporter headers and timeouts use the standard OTel variables. Only HTTP/protobuf is supported; gRPC and HTTP/JSON protocol settings are rejected with a stderr diagnostic. Without any endpoint, logging stays local. `OTEL_SERVICE_NAME` overrides a `service.name` in `OTEL_RESOURCE_ATTRIBUTES`; otherwise the default is `myko-server`. Give each host application its own service name. Log batches use the SDK's bounded queue and are flushed when the guard drops. Exporter diagnostics remain local to prevent recursive log export. --- slug: /hyphae/hyphae/readme title: hyphae source: hyphae --- # hyphae A high-performance concurrent reactive programming library for Rust. ## Features - **Fast reads** - Atomic value snapshots via `arc-swap` - **Type-safe** - Compile-time checking with heterogeneous cell combinations - **Thread-safe** - Safe concurrent access across threads - **Dependency tracking** - Inspect and visualize cell relationships ## Quick Start ```rust use hyphae::{ Cell, JoinExt, MapExt, Materialize, Mutable, Signal, Watchable, flat, }; let x = Cell::new(5); let y = Cell::new(10); // join stays lazy, then creates its required fan-in coalescing boundary // when the chain is materialized. let sum = x.clone().join(y).map(flat!(|a, b| a + b)).materialize(); // Subscribe to changes let _guard = sum.subscribe(|signal| { if let Signal::Value(v) = signal { println!("Sum: {}", v); } }); x.set(20); // prints "Sum: 30" ``` ## Pipelines vs Cells Pure operators (`map`, `filter`, `try_map`, `tap`, `map_ok`, `map_err`, `catch_error`, `unwrap_or`) return a `Pipeline` — an uncompiled chain that fuses closures at compile time. Call `.materialize()` to compile the chain into a `Cell` you can subscribe to. There is no `Pipeline::subscribe` by design: callers make the memoization decision explicit. All pipeline surfaces use `Materialize`. The seedness parameter selects the result shape: `Definite` produces `Cell`, while `Empty` produces `Cell>` for operators such as `filter` that may suppress their initial emission. Import `Materialize` to call `.materialize()`. Operators that need state or multiple sources (`debounce`, `buffer_*`, `join`, `merge`, `switch_map`, and others) are lazy pipelines too. Their state and any required fan-in boundary are created only when the pipeline is installed. Call `.materialize()` at the point where you need a cached value, `get()`, or `subscribe()`. Pipeline values are deliberately not `Clone`. To retain a source after an operator consumes it, clone the source handle before building the pipeline. To share one pipeline installation across several consumers, use `.share()`, or materialize once and clone the resulting `Cell`. Upgrading from Hyphae 2.x? See [Migrating to Hyphae 3.0](https://github.com/ignition-is-go/hyphae/blob/main/docs/migrating-to-v3.md). ## Operators Transform, combine, and filter reactive streams. Pure operators below (`map`, `filter`, `catch_error`) return pipelines — call `.materialize()` when you need a cell. Stateful operators such as `scan`, `debounce`, and `throttle` are lazy pipelines too. ```rust use std::time::Duration; use hyphae::{ CatchErrorExt, DebounceExt, FilterExt, MapExt, Materialize, ScanExt, ThrottleExt, }; let doubled = x.map(|v| v * 2).materialize(); let filtered = x.filter(|v| *v > 10).materialize(); let running_sum = numbers.scan(0, |acc, x| acc + x).materialize(); let debounced = input.debounce(Duration::from_millis(100)).materialize(); let throttled = input.throttle(Duration::from_millis(50)).materialize(); let safe = fallible.catch_error(|_| default).materialize(); ``` ## Reactive Collections ```rust use hyphae::{CellMap, Gettable, Materialize}; let users = CellMap::::new(); let admin = users.get(&"admin".to_string()).materialize(); users.insert("admin".to_string(), User::new()); assert!(admin.get().is_some()); // updates automatically ``` Reactive collection views (`get`, `entries`, `items`, `keys`, `size`, `len`, and `diffs`) expose pipelines too. Some currently reuse an internal cached cell, so their terminal `.materialize()` is a no-op today; keeping the boundary in the public contract lets those implementations become fully deferred later without another API break. ## Map Queries vs CellMaps Pure `CellMap` operators build consuming, non-`Clone` `MapQuery` plans. A plan's shape is expressed with associated types (`MapQuery`), and semantic operators make cardinality and key behavior explicit: - `select` / `select_by` filter without changing keys; - `map_values` / `filter_map_values` preserve keys; - `map_entries` / `filter_map_entries` may rekey; and - `flat_map_entries` emits locally keyed one-to-many results. `map_entries` and `filter_map_entries` require globally unique output keys and panic synchronously after pre-mutation validation on collision. `flat_map_entries` changes output identity to `(source_key, local_key)`: this prevents cross-source collisions, but each source row must emit a local key at most once. See the migration guide and [`map_query`](https://docs.rs/hyphae/latest/hyphae/map_query/) for the full contracts. Plans compose without observable intermediate maps. `.materialize()` is the sole observation boundary: it consumes the plan, installs one subscription per interned physical root, and returns the cached, subscribable output `CellMap`. Materialize once and clone that `CellMap` when several consumers should share one installation. ```rust use hyphae::{CellMap, MapQuery, traits::{InnerJoinExt, MapValuesExt}}; let users = CellMap::::new(); let scores = CellMap::::new(); users.insert("u1".into(), "alice"); scores.insert("u1".into(), 42); let view = users .clone() .inner_join(scores.clone()) .map_values(|user_id, (name, score)| format!("{user_id}:{name}:{score}")) .materialize(); assert!(view.contains_key(&"u1".to_string())); ``` Typed foreign-key joins use one zero-sized `ForeignKeyRelation` marker for each semantic relationship. Its extractor returns `Some(foreign_key)` for a present relationship and `None` for an absent optional relationship. The join targets `IdFor::MapKey`; it does not infer identity from the current left payload. Repeated uses of the same raw physical right source and relation reuse one relationship index within a materialized plan. Query closures must be deterministic, externally side-effect-free, and nonblocking. They may be invoked repeatedly or concurrently; invocation count, order, and thread are not API guarantees. Output diff publication remains deterministic by logical input/emission ordinal and synchronously settled before the initiating source mutation returns. Hash-map and initial-snapshot iteration order are not API contracts. With the `scheduler` feature on native targets, eligible expensive join regions adaptively use Hyphae's shared dedicated Rayon pool. Builds without `scheduler` and wasm execute sequentially. `HYPHAE_WORKER_THREADS=0` disables the native pool; `HYPHAE_WAVE_THREADS` remains a compatibility fallback. ## Async Support ```rust use hyphae::{Cell, AsyncWatchableExt}; let cell = Cell::new(0); let mut stream = cell.to_stream(); while let Some(value) = stream.next().await { println!("Got: {}", value); } ``` Requires the `async` feature flag. ## Profiling The `profiling` feature is hyphae's single observability switch. It costs nothing per cell and ~1% on the hot path: - `Cell::notify` / `write_value` / `fanout` become `#[inline(never)]`, so sampling profilers resolve them as distinct frames instead of one folded symbol. - Each fanout emits a `tracing` span (`hyphae.fanout`) tagged with the cell's `id` and `name` (set names with `Cell::with_name`). hyphae only emits spans; the application attaches the subscriber (`tracing-flame`, `tracing-tracy`, …). - `hyphae::profiling::pass` / `take_report` tally per-cell re-fires inside a measured propagation pass. For live-cell counts and memory attribution, use a heap profiler (jemalloc/`jeprof`, or `pprof`) rather than an in-process registry. See [`docs/profiling.md`](docs/profiling.md). ## License MIT OR Apache-2.0