Layer 4–6 — Reasoning, Safety, Observability
Part of the OpenRAL public-symbol inventory. Hand-curated;
(LNN)markers are refreshed bytools/refresh_methods_linenos.py.
Layer 6 (Observability) is fully shipped — traces + metrics + structlog→OTLP log bridge, with W3C TraceContext propagation helpers for cross-process correlation (Python ↔ ROS 2 ↔ C++ safety kernel). Layer 4 (Reasoner) ships the live ReasonerCore direct-dispatch loop below; Layer 5 (C++ safety kernel) is still planned.
python/reasoner/src/openral_reasoner/tool_use.py
Typed LLM tool-use clients (direct-dispatch surface); the ReasonerToolCall union is the sole planner output.
- module constant
DEFAULT_SYSTEM_PROMPT: str— Base S2 system prompt: one-tool-per-tick, safe skill selection, never bypass e-stop, exact field names,waitwhile a skill runs. Deployments may override. (L92) - module constants
ANTHROPIC_BASE_URL/OPENROUTER_BASE_URL/OLLAMA_BASE_URL/VLLM_BASE_URL/GEMINI_BASE_URL/XAI_BASE_URL/DEEPSEEK_BASE_URL/HUGGINGFACE_BASE_URL: str— Named-endpoint base URLs; canonical copy now lives inopenral_core.schemas, re-exported here unchanged. - module constant
_ENDPOINT_PRESETS: dict[str, ReasonerEndpointPreset]— Back-compat alias ofopenral_core.REASONER_ENDPOINT_PRESETS: per-endpointurl/dialect/auth_required/timeout_s/tool_choicefor eachOPENRAL_REASONER_ENDPOINTname. The curated model-first path takes only the endpoint properties from it; the uncurated path takes all five. (L649) - module constant
SYSTEM_PROMPT_ENV_VAR: str = "OPENRAL_REASONER_SYSTEM_PROMPT"(L446) — env var that overrides the base operating brief; honoured byresolve_reasoner_system_prompt. render_robot_context_prompt(capabilities: RobotCapabilities | None, *, base_prompt=DEFAULT_SYSTEM_PROMPT) -> str(L335) — Appends a deterministic## THIS ROBOTbody-awareness block (embodiment, locomotion, manipulation/sensing, payload, control modes) to the prompt;Nonereturnsbase_promptunchanged. No cell geometry is listed: place surfaces are measured, not surveyed.resolve_reasoner_system_prompt(capabilities: RobotCapabilities | None, *, env=None) -> str(L449) — Composes the full reasoner system prompt (env override or default base brief +## THIS ROBOTblock).envis injectable for tests; called fromReasonerNode.on_configure.class ToolUseClient(Protocol)(L496) — LLM tool-use client contract:model_id, optionallast_prompt_tokens. RaisesROSReasonerInvalidPlanon a bad/mismatched tool call,ROSPlanningErroron transport failure.select_tool(self, *, context_text, palette, system_prompt=DEFAULT_SYSTEM_PROMPT) -> ReasonerToolCall(L518) — Pick exactly oneReasonerToolCallforcontext_text.describe_image(self, *, image_jpeg, question) -> str(L550) — Ask the model a free-text question about a single camera frame.class OpenAICompatibleToolUseClient— OpenAI-compatible tool-use client.tool_choice='auto'covers endpoints that reject'required'(retries once on a prose reply);max_tokensoptionally caps completion tokens, else the endpoint default applies.- module constant
REASONER_MODEL_ENV: str = "OPENRAL_REASONER_MODEL"(L584) - module constant
REASONER_ENDPOINT_ENV: str = "OPENRAL_REASONER_ENDPOINT"(L585) - module constant
REASONER_API_KEY_ENV: str = "OPENRAL_REASONER_API_KEY"(L586) - module constant
REASONER_DIALECT_ENV: str = "OPENRAL_REASONER_DIALECT"(L587) - module constant
REASONER_MAX_TOKENS_ENV: str = "OPENRAL_REASONER_MAX_TOKENS"(L588) - module constant
REASONER_TIMEOUT_ENV: str = "OPENRAL_REASONER_TIMEOUT_S"(L589) — model-first env names (ADR-0088). - module constant
REASONER_TOOL_CHOICE_ENV: str = "OPENRAL_REASONER_TOOL_CHOICE"(L596) —auto/requiredoverride for the uncurated bare-URLopenaipath only; validated on every path viaopenral_core.parse_reasoner_tool_choice, and ignored with areasoner.tool_choice_ignoredwarning elsewhere. build_tool_use_client_from_env() -> ToolUseClient— Model-first client factory:OPENRAL_REASONER_MODELresolvesopenral_core.REASONER_MODELSfor dialect/endpoint/auth/hosting/tool-choice/token-cap;OPENRAL_REASONER_ENDPOINToverrides location (a named preset, a bare URL, ormanaged);API_KEY/MAX_TOKENS/TIMEOUT_Soverride the rest.DIALECTis needed only for a bare URL and always wins when set. No hidden model default. (L652)class AnthropicToolUseClient(L1464) — Anthropic SDK-backed client; builds/reuses one client/HTTP pool and marks the static system/tools prefix cacheable.select_tool(self, *, context_text, palette, system_prompt=DEFAULT_SYSTEM_PROMPT) -> ReasonerToolCall(L1531) — Call Anthropic and decode the resulting tool payload.describe_image(self, *, image_jpeg, question) -> str(L1578) — Ask the Anthropic model a free-text question about a camera frame._anthropic_response_text(response) -> str— Concatenate all text blocks so thinking-enabled responses do not lose an answer after a leading thinking block.class OpenAICompatibleToolUseClient(L1625) — OpenAI-compatible client with cached SDK/HTTP pool, endpoint/tool-choice/token-cap configuration, and image-description support.select_tool(self, *, context_text, palette, system_prompt=DEFAULT_SYSTEM_PROMPT) -> ReasonerToolCall(L1700) — Call the OpenAI-compatible endpoint and decode the tool call.describe_image(self, *, image_jpeg, question) -> str(L1775) — Ask an OpenAI-compatible model a free-text question about a frame._tool_palette_to_anthropic_tools(palette) -> list[dict]— Render the closed palette, includingWaitTool; per-skill names use collision-resistantexecute_rskill__<slug>_<sha1-8>._tool_palette_to_openai_tools(palette) -> list[dict]— Convert the same surface to OpenAI function shape without leaking the Anthropic-onlyinput_schemakey._decode_tool_payload(*, tool_name, arguments, palette) -> ReasonerToolCall— Validate provider output against the union + palette; per-skill names resolve through the same hashed mapping used to render them.- module constant
_PER_SKILL_TOOL_PREFIX: str = "execute_rskill__"— prefix the decoder matches on to identify per-skill tool calls. (L970) - module constant
_LLM_TOOL_NAME_MAX_LEN: int = 64— Anthropic + OpenAI tool-name regex limit; long HF Hub ids are sha1-suffix-truncated to fit. (L973) _skill_id_to_tool_name(rskill_id: str) -> str— Map a<owner>/<repo>id into a collision-resistant 64-char-maxexecute_rskill__<slug>_<sha1-8>name. (L976)_format_skill_tool_description(entry: RSkillToolEntry) -> str— Render the skill's id + description + actions + objects + scenes into the NL string the LLM scores. (L993)_drop_property(schema: dict, name: str) -> dict— Return a copy of a JSON Schema dict withnamestripped from bothpropertiesandrequired. Used to droprskill_idfrom per-skillExecuteRskillToolschemas. (L1348)- module constant
_TOOL_ADAPTER: TypeAdapter[ReasonerToolCall](L332) — cachedTypeAdapter(ReasonerToolCall)used to validate a decoded tool payload against the discriminated union.
python/reasoner/src/openral_reasoner/cosmos3.py
NVIDIA Cosmos 3 reasoner backend (OPENRAL_REASONER_MODEL=cosmos3-edge), the physical-AI-native S2 planner. Runs the on-device 4B Edge tier behind an OpenAI-compatible endpoint, so the typed tool-use contract is unchanged. Companion boot helper: tools/cosmos3_reasoner_sidecar.py.
- module constant
COSMOS3_BASE_URL: str—http://127.0.0.1:8901/v1; dedicated port so the sidecar never collides with a user-runvllm serveon :8000. (L68) - module constant
DEFAULT_COSMOS3_MODEL: str—nvidia/Cosmos3-Edge, served id of the curatedcosmos3-edgeregistry entry. (L74) - module constants
AUTOSTART_ENV/BOOT_TIMEOUT_ENV/SIDECAR_SCRIPT_ENV/DEFAULT_BOOT_TIMEOUT_S— env knobs:OPENRAL_COSMOS3_AUTOSTART(0disables managed spawn),OPENRAL_COSMOS3_BOOT_TIMEOUT_S(first boot provisions a venv + downloads weights; default 1800 s),OPENRAL_COSMOS3_SIDECAR(boot-helper path override). (L79) - module constant
BOOT_TIMEOUT_ENV: str = "OPENRAL_COSMOS3_BOOT_TIMEOUT_S"(L80) - module constant
SIDECAR_SCRIPT_ENV: str = "OPENRAL_COSMOS3_SIDECAR"(L81) - module constant
DEFAULT_BOOT_TIMEOUT_S: float = 1800.0(L82) — default boot timeout in seconds. - module constant
_LOOPBACK_HOSTS: frozenset[str](L84) —{"localhost", "127.0.0.1", "::1", "0.0.0.0"}, hostnames_managed_porttreats as loopback. find_cosmos3_sidecar_script() -> Path— Locatetools/cosmos3_reasoner_sidecar.py(env override or repo-parent walk); raisesROSConfigErrorwhen an explicit override path is missing or the walk finds nothing. (L87)_managed_port(base_url) -> int | None— Explicit port of a loopback base URL, orNone(unmanaged, e.g. a portless reverse proxy). Autostart needs an explicit loopback port so spawn and readiness probe agree. (L119)class Cosmos3ToolUseClient(OpenAICompatibleToolUseClient)— Managed local sidecar client over the inherited OpenAI-compatible wire path; adds probe/spawn/readiness/teardown.warm()letsReasonerNode.on_configurestart the sidecar at bringup instead of on the first tick, overlapping the model load with other startup work. Idempotent: reuses a still-booting child instead of duplicate-spawning. (L155)warm(self) -> None(L342) — Start the managed sidecar now instead of on the first tick.select_tool(self, *, context_text, palette, system_prompt=DEFAULT_SYSTEM_PROMPT) -> ReasonerToolCall(L361) — Ensure the managed server is up, then run the OpenAI-compatible path.describe_image(self, *, image_jpeg, question) -> str(L376) — Ensure the managed server is up, then run the OpenAI-compatible path.close(self) -> None(L328) — Terminate the server child if (and only if) we spawned it.
python/reasoner/src/openral_reasoner/palette.py
Closed-set ToolPalette + builder. Three tool variants (reload_gst_pipeline / lifecycle_transition / emit_prompt) are always available; execute_skill is gated by the installed-rSkill registry filtered by RobotCapabilities + license posture.
class RSkillToolEntry(BaseModel)(L240) — Frozen per-skill record surfaced to the LLM as one tool:rskill_id,description,actions,objects,scenes, mirrored from the matchingRSkillManifestat palette-build time.class ContinuousDetectorEntry(BaseModel)(L110) — Frozen coverage record for amode: continuousdetector, surfaced to the LLM as coverage (not a tool) so it can read tracked objects from world state and reservelocate_in_viewfor the long tail.class OnDemandDetectorEntry(BaseModel)(L217) — Frozen record for amode: on_demandopen-vocab locator, surfaced as a selectablelocate_in_viewoption — a read-only prompt-able tool, never an ExecuteRskill policy.detector_alias(rskill_name) -> str(L138) — Short LLM-/operator-facing detector id: strips theOpenRAL/org +rskill-kind prefixes. Single source of truth for the alias the reasoner routes on.detector_service_segment(alias) -> str(L151) — ROS-safe service-namespace segment for an alias (hyphens → underscores), so the locate service lives at/openral/perception/<segment>/locate_in_view.locate_in_view_service(detector, *, default="") -> str(L200) — Resolves thelocate_in_viewservice for a (possibly empty) selector: empty →default; unresolved alias → the legacy single-detector service; else the namespaced service.resolve_locate_in_view_detector(detector, *, known_aliases) -> str(L162) — Normalizes an LLM-supplieddetectorvalue to""when it names no configured on-demand locator, so an unrecognised alias degrades to the deployment default instead of a service that never existed.class ToolPalette(BaseModel)(L279) — Frozen palette presented to the LLM each tick: primaryskills, back-compatexecute_rskill_ids(auto-derived), sensor/node ids,continuous_detectors(coverage), and availability flags gating the read-only tools —spatial_memory_available(recall_object/resolve_place),detector_available(locate_in_view, withon_demand_detectorsas its options),scene_query_available(query_scene, independent of detection),memory_available(memory_write/memory_search). A cross-validator rejectsskills/execute_rskill_idspassed with disagreeing ids.build_tool_palette(*, installed_skills, robot_capabilities, sensor_ids=(), node_ids=(), commercial_deployment=False, spatial_memory_available=False, detector_available=False, scene_query_available=False, task_progress_available=False, memory_available=False) -> ToolPalette(L412) — Builds the palette: a skill qualifies by role/kind/capability/embodiment/license checks. Acontinuousdetector becomes coverage only;on_demandbecomes alocate_in_viewoption;segmenteris dropped and surfaced nowhere. Output is id-sorted for a deterministic tool schema.task_space_disagreement(manifest, description, hal_mode, legacy_ok) -> str | None(L52) — Warn-only shadow gate: compares the canonicaltask_space_compatibleverdict against the caller'slegacy_ok, returning a warning string only on disagreement (Noneotherwise, including for non-actuating skills). Surfaces cross-layer mismatches without changing the drop/publish decision.
python/reasoner/src/openral_reasoner/grounding.py
Grounds an ExecuteRskillTool's named targets into the goal's declarations (real pick-and-place design §2.2-2.3): the reasoner names, perception grounds, the producer measures. Pure, ROS-free; never supplies a region. A place target is an optional hint: the real producer measures the surface under the carried payload without one.
DECLARATION_TIMEOUT_MARGIN_S = 10.0(L44) — Seconds a grounded declaration'stimeout_sexceeds the goal's patience ceiling (then capped at the declaration'sMAX_TIMEOUT_S).gripper_hands— re-exported fromopenral_core.gripper_hands(moved there so the HAL's approach-armed grasp target groups hands without importing the reasoner; seedocs/methods/00-core-schemas.md).ground_grasp_target(ref, *, live_objects, scene_graph, base_frame, default_contact_links, patience_s, pad_m) -> GraspDeclaration(L146) —ref.object_idset → that spatial-memory node'sbbox_3d; else the single live lifted detection whose label equalsref.label(case-insensitive). The box, padded bypad_mon every side (downward too, so the support layer is inside it: a search hint with no support semantics; the producer measures the support) and gravity-aligned (identity quaternion inbase_frame), becomessearch_box;target_id="obj:<label or node_id>",region=None. RaisesROSReasonerInvalidPlanon no match, more than one match withoutobject_id, an unknown node id, a missing box, a box in a frame other thanbase_frame(no tf2 here: a fixed-base cell's lift must run in its base frame), no gripper joint, nocontact_linksnamed on a robot with more than one hand (defaulting would exempt every hand), named links that are not gripper child links of ONE hand, or only part of one hand's links (that would exempt one finger and the kernel would stop the grasp; the refusal lists the whole hand). Named links are accepted in any order and the declaration carries the hand's links in manifest order.default_contact_linksisgripper_hands(...)(a barestrelement is a one-link hand); an emptyref.contact_linksdefaults to the hand of a single-hand robot.ground_place_target(ref, *, live_objects, scene_graph, base_frame, patience_s, pad_m) -> PlaceDeclaration(L253) — The mirror ofground_grasp_targetfor an OPTIONAL named surface:ref.object_id/ref.place_node_idset → that spatial-memory node'sbbox_3d; else the single live lifted detection whose label equalsref.label. The padded, gravity-aligned box becomessearch_box(a hint the producer refuses a patch outside of);target_id="surface:<label or node_id>",region=None. RaisesROSReasonerInvalidPlanon no match (incl. no detector), more than one match without a node id, an unknown node id (incl. no spatial memory), a missing box or one outsidebase_frame— the hint is optional, but a supplied one that does not ground refuses the goal (message: "the place hint could not be grounded…" + omitplace_targetto place on the measured surface), never a fall-back to placing anywhere.ground_workspace(source, destination, *, anchor_xyz, base_frame, pad_m, headroom_m, evidence_ref) -> PlaceRegion(L342) — AGraspDeclaration.workspace_boxfrom boxes: the gravity-aligned hull of the source fixture and the destination, padded, plusheadroom_mon top; an axis wider than twiceMAX_WORKSPACE_HALF_EXTENT_Mis cut to that window aroundanchor_xyz(the target). RaisesROSReasonerInvalidPlanpast the volume cap. Fed by sim ground truth (tools/derive_scene_inputs.py) or, on a real cell, scene-graph surface boxes.
python/reasoner/src/openral_reasoner/spatial_query.py
Read-only spatial-memory query bridge: maps a RecallObjectTool / ResolvePlaceTool to a query, runs it against an injected backend, and renders an LLM-readable result. Layer-4 module; backend is duck-typed, not imported from openral_world_state.
class SpatialMemoryQuerier(Protocol)(L49) — Read-only query surface; structurally satisfied byopenral_world_state.SpatialMemory.recall_object(self, query, *, now_ns) -> RecallObjectResult(L52) — Recall objects matchingquery(empty result = not found).resolve_place(self, query, *, from_node_id=None) -> ResolvePlaceResult(L56) — Resolve a place/room/agent reference (raisesROSObjectNotInMemoryon miss).to_scene_graph(self) -> SceneGraph(L62) — Immutable snapshot of the current graph (for telemetry / dashboard).SpatialQueryTool: TypeAlias—RecallObjectTool | ResolvePlaceTool(the read-only ReasonerToolCall variants this bridge dispatches).recall_object_tool_to_query(call) -> RecallObjectQuery(L67) — tool → query mapper.resolve_place_tool_to_query(call) -> ResolvePlaceQuery(L72) — tool → query mapper.format_recall_object_result(query_text, result, *, blocked_node_ids=frozenset()) -> str(L81) — Render results as LLM-readable text; a miss is reported as text, never a fabricated pose.blocked_node_idsrenders a match whose approach failed grid refinement as BLOCKED instead of a pose.format_resolve_place_result(reference, result) -> str(L125) — render aResolvePlaceResultas an LLM-readable text block.run_spatial_query_detailed(call, querier, *, now_ns, from_node_id=None, refine_approach=None) -> SpatialQueryOutcome(L150) — Execute a read-only tool call; render text and report whether the query matched, so a miss can be escalated without re-parsing the rendered text.class SpatialQueryOutcome(NamedTuple)(L136) —(text, found).foundisTruewhenrecall_objecthad ≥1 match (even if grid-BLOCKED) orresolve_placeresolved; drives the reasoner's recall→locate_in_viewescalation.run_spatial_query(call, querier, *, now_ns, from_node_id=None, refine_approach=None) -> str(L221) — Execute a read-only tool call and render the result;ROSObjectNotInMemory→ "not in memory".refine_approach(duck-typed, so this L4 module never imports L2) adjusts eachrecall_objectmatch's approach viewpoint;Nonemarks it BLOCKED. Thin wrapper returning only.text.ApproachRefiner(TypeAlias =Callable[[ApproachViewpoint, tuple[float, float, float]], ApproachViewpoint | None]) — occupancy-grid refinement callback contract; the reasoner node wiresrefine_approach_poseover its latched/mapsubscription.
python/reasoner/src/openral_reasoner/active_search.py
Bounded active object search over the scene graph (pure-Python, openral_core only).
class SearchBudget(BaseModel)(L25) — frozen;max_candidates(1–50),max_attempts(1–50). The bound.class SearchCandidate(BaseModel)(L39) —place_node_id, goal: Pose6D, open_container_id: str | None, reason, rank ∈ [0,1].plan_active_search(graph, *, target_text, budget) -> list[SearchCandidate](L60) — Ranked frontier of places to check (occluding containers, then containers, then places), truncated tobudget.max_candidates;[]when nowhere to search (hands off to a human). The LLM prioritizes among candidates.class SearchProgress(L114) — attempt counter against aSearchBudget. The runaway bound.attempts(self) -> int(L128) — Number of search steps consumed so far.exhausted(self) -> bool(L133) — True once the attempt budget is spent (hands off to a human).record_attempt(self) -> bool(L137) — Consume one attempt;Truewhile budget remains, elseFalse.reset(self) -> None(L142) — Reset the counter (e.g. on a fresh operator goal).format_search_frontier(candidates, target_text) -> str(L147) — LLM-readable frontier text (empty → "hand off to a human").
python/reasoner/src/openral_reasoner/completion.py
VLM adjudication helpers (pure, no rclpy). Importable and testable without a ROS install.
- COMPLETION_QUESTION: str (L161) — prompt template for the ambiguous reward band, formatted with task=active.text.
- module constant _NEGATIONS: tuple[str, ...] (L165) — negation tokens parse_yes_no checks for.
- module constant _AFFIRMATIVES: tuple[str, ...] (L183) — affirmative tokens parse_yes_no checks for.
- parse_yes_no(answer: str) -> bool (L196) — Parse a VLM free-text answer: True iff it contains an affirmative token without a negation token. Token-based, not substring, to avoid false-completing on "No. It is done." or "abandoned" (contains "done"); typographic apostrophes fold to ASCII first. False on empty or ambiguous input.
- image_msg_to_jpeg(*, data: bytes, height: int, width: int, encoding: str, flip_180: bool = False) -> bytes (L253) — Convert a raw sensor_msgs/Image payload to JPEG via numpy + PIL (no cv_bridge); supports "rgb8"/"bgr8". flip_180 rotates 180° before encoding to match bottom-up LIBERO/MuJoCo frames. Raises ValueError on unsupported encoding.
- is_frame_fresh(*, age_s: float, max_age_s: float) -> bool (L22) — Whether a cached completion frame is recent enough to adjudicate: True iff max_age_s <= 0 or age_s <= max_age_s. A stale frame degrades to "cannot adjudicate", never a false verdict.
- is_reward_wake(*, source: str, severity: int, severity_fail: int) -> bool (L129) — Classify a FailureTrigger as a reward-watcher wake: True iff source == "critic" and severity >= severity_fail. Used to cancel an in-flight execute_rskill goal on the reward signal rather than waiting for deadline_s.
- resolve_band_edges(*, contract_threshold: float | None, contract_floor: float | None, fallback_threshold: float, fallback_floor: float) -> tuple[float, float] (L50) — Three-tier verdict band edges: the active reward model's (success_threshold, check_floor) when both present, else the system fallback (never mixed).
- resolve_patience_s(*, override: float | None, contract_default: float | None, legacy_deadline_s: float) -> float (L93) — Patience ceiling for a dispatch: LLM patience_s override > reward model's default_patience_s > legacy deadline_s (0.0 → runner resolves its own ceiling).
python/reasoner/src/openral_reasoner/mission.py
Typed sequential task queue for multi-task deploy goals. Reasoner-internal bookkeeping (no rclpy/Pydantic); the node drives transitions, the ContextRenderer renders the ## MISSION ledger.
- TaskStatus (TypeAlias = Literal["pending","active","verifying","done","abandoned"]) — subtask lifecycle; done/abandoned terminal (never re-queued).
- VerdictAction (TypeAlias = Literal["complete","abandon","retry","vlm_check"]) — the reward-gate decision: complete (auto-pass), vlm_check (ambiguous band — caller adjudicates via describe_image), abandon (ladder exhausted), retry.
- DEFAULT_MAX_ATTEMPTS: int = 3 (L49) — default per-task attempt cap before the gate abandons + hands off.
- DEFAULT_MAX_SUBDIVIDE_DEPTH: int = 2 (L52) — max re-decomposition depth; a task already at this depth is refused subdivision, terminating the ladder in human-handoff.
- DEFAULT_MAX_TASK_LOCATE_ATTEMPTS: int = 3 (L63) — per-task locate_in_view cycle budget before abandonment. Distinct from SearchProgress's miss budget, which resets on a hit and so cannot bound a hit-looping search.
- module constant _ACTIVE_STATES: frozenset[TaskStatus] (L240) — {"active", "verifying"}.
- module constant _TERMINAL_STATES: frozenset[TaskStatus] (L241) — {"done", "abandoned"}.
- evaluate_task_verdict(*, ok: bool, progress_now: float, success_threshold: float, check_floor: float, attempts: int, max_attempts=DEFAULT_MAX_ATTEMPTS, success_now: float | None = None) -> tuple[VerdictAction, str] (L156) — Three-tier reward gate. Gates on the PROGRESS head, not the (compressed) success head: progress_now >= success_threshold → "complete"; check_floor <= progress_now < success_threshold → "vlm_check"; below floor → attempts ladder ("abandon" at cap, else "retry"). ok=False always goes to the ladder. success_now is an optional secondary signal in the verdict text, never overriding the progress band.
- class TaskLocateBudget (dataclass, slots) (L75) — per-task locate_in_view cycle budget. Held by reasoner_node as _task_locate_budget.
- task_id(self) -> str | None (L128) — the task the budget is currently tracking (for persistence).
- count(self) -> int (L104) — locate cycles charged against the current task so far.
- reset(self) -> None (L108) — clear the budget (new task / real progress).
- restore(self, task_id, count) -> None (L113) — restore a persisted counter (reasoner restart resume).
- charge(self, task_id) -> bool (L132) — Count one locate cycle for the active task (auto-resets on task_id change); True once the count exceeds DEFAULT_MAX_TASK_LOCATE_ATTEMPTS.
- reason(self, query) -> str (L144) — specific abandonment reason for the ledger, e.g. could not confirm 'teapot' in view after 3 locate attempts without a skill dispatch.
- class TaskState (dataclass, slots) (L245) — one subtask: fields task_id, text, status=pending, attempts=0, last_rskill_id, last_trace_id, last_verdict, depth=0 (depth = re-decomposition level).
- class MissionState (L274) — ordered queue, ≤1 task active/verifying.
- from_prompt(cls, text) -> MissionState (L306) — seeds the operator goal as a single active task; the LLM decomposes via decompose_mission.
- tasks(self) -> tuple[TaskState, ...] (L318) — snapshot of every task, in order.
- active(self) -> TaskState | None (L326) — the currently active or verifying task, or None.
- is_empty(self) -> bool (L330) — True when the mission carries no tasks (empty/whitespace prompt).
- is_complete(self) -> bool (L334) — True when every task is terminal (done/abandoned).
- has_started(self) -> bool (L342) — True when any task is terminal or the active task has been attempted; gates a safe decompose_mission populate-replace.
- record_attempt(self, *, rskill_id, trace_id=None) -> None (L366) — note a dispatch against the active task (increments attempts).
- mark_verifying(self) -> None (L376) — move the active task active → verifying (a skill returned; gating).
- complete_active(self, verdict) -> TaskState | None (L382) — mark the active task done and activate the next pending task.
- abandon_active(self, reason) -> TaskState | None (L389) — mark the active task abandoned (ladder exhausted / unverifiable).
- rearm_active(self) -> TaskState | None (L396) — move the active task verifying → active so dispatch/subdivide resumes after a subdivision offer.
- subdivide_active(self, subtasks, *, max_depth=DEFAULT_MAX_SUBDIVIDE_DEPTH) -> TaskState | None (L418) — Flat-splice the blocked active task in place with finer children t<n>.1, t<n>.2, … at depth+1, activate the first; returns None (caller falls back to abandon_active) with no active task, empty subtasks, or the depth bound reached.
- to_state_dict(self) -> dict[str, object] (L485) — Full round-trippable snapshot (every TaskState field) for crash-safe ladder persistence, unlike the lossy to_summary.
- from_state_dict(cls, state) -> MissionState (L517) — rebuild a mission from to_state_dict output (exact resume); raises ValueError on malformed input.
- to_summary(self) -> dict[str, object] (L544) — JSON-able snapshot stamped on the reasoner.tick span as reasoner.mission_json for the dashboard Mission card; children carry dotted ids t<n>.1 for indenting under their parent.
- render(self) -> str (L573) — the ## MISSION ledger: done ✓ / active ▶ / verifying ? / abandoned ✗ + pending count; children indented by depth.
python/reasoner/src/openral_reasoner/node_policy.py
Pure prompt-handling policy for the reasoner node (no rclpy) — the two consequential _on_prompt decisions, single-sourced and unit-testable without a ROS install.
- module constant
CASCADE_PROMPT_SOURCES: frozenset[str](L29) — the six frame_ids of the reasoner's own cascade re-prompts (spatial_memory,detector,scene_vlm,reward_monitor,memory,mission). Both the search-bound/streak reset guard and the mission-rebuild guard key off this one set. should_rebuild_mission(source, metadata_json, mission) -> bool(L34) — Whether an inbound prompt replaces the mission queue: cascade → never; no/empty/finished/not-yet-started mission → yes; a genuinely in-progress mission → only with top-level"new_goal": truemetadata (an operator reply must not silently discard the in-flight queue).
python/reasoner/src/openral_reasoner/persistence.py
Crash-safe ladder persistence (pure, no rclpy): the mission ledger + every replanning-ladder bound snapshotted after each mutation and reloaded at configure, so a restart resumes the ladder instead of resetting every cap mid-mission.
- module constant
_SCHEMA_VERSION: Literal["0.1"](L33) — on-disk schema version stamped into every snapshot;load_ladder_staterefuses a mismatch. class ReasonerLadderState(Pydantic v2, extra-forbid) (L36) — versioned on-disk boundary carrying everything a restarted reasoner needs:mission: MissionState | None,subdivide_offered: set[str],collective_nudges: dict[str, int],locate_task_id: str | None,locate_count: int.save_ladder_state(path, state) -> None(L93) — atomic JSON write (tmp +os.replace; a crash mid-write leaves the previous snapshot intact). Versioned (schema_version: "0.1").load_ladder_state(path) -> ReasonerLadderState | None(L115) —Noneon absent / corrupt / version-mismatched snapshot; caller starts fresh and logs rather than resuming from a bad snapshot.
python/reasoner/src/openral_reasoner/memory.py
The self-maintained MEMORY.md file model: persistent semantic memory (preferences, lessons, home facts, object-location log, open tasks). Summarized inline in context.py's entry above; this is the file's own symbol inventory.
- module constant
_SECTION_TITLES: dict[MemorySection, str](L28) — the five section headings ("Home Map / Places","User Preferences","Learned Lessons / Corrections","Object-Location Log","Open Tasks / Commitments") MEMORY.md is organized under. - module constant
_TITLE_TO_SECTION: dict[str, MemorySection](L35) — reverse of_SECTION_TITLES, for parsingfrom_markdown. - module constant
_ENTRY_RE(L38) — regex matching one round-trip-stable MEMORY.md entry line (- [imp:<float> ts:<iso> st:<status>] <content>). - module constant
_SCHEMA_VERSION: str = "0.1"(L40) — on-disk MEMORY.md schema marker. class MemoryEntry(L53) — One remembered fact in aMemoryStore.render_line(self) -> str(L62) — Render the entry as one round-trip-stable MEMORY.md line.class MemoryStore(L68) — Ordered set ofMemoryEntryrendered to / parsed fromMEMORY.md.from_markdown(cls, text) -> MemoryStore(L78) — Parse aMEMORY.mdbody. Unrecognized lines are ignored (lenient).to_markdown(self) -> str(L113) — The full on-diskMEMORY.md(title + schema marker + sections).to_context_block(self, *, cap=None) -> str(L132) — The## MEMORYblock the reasoner injects into its context;caprenders only the top-capentries by importance then recency.consolidate(self) -> list[MemoryEntry](L157) — Merge exact-duplicate facts, keeping the best; return the removed entries for archiving.entries(self) -> tuple[MemoryEntry, ...](L180) — A snapshot of the current entries, in order.apply(self, *, op, section, content, importance, target, now) -> MemoryEntry | None(L186) — Apply one explicitadd/update/supersede/deleteedit. Returns an entry to archive (orNone).search(archive, *, query, section, limit) -> list[MemoryEntry](L232) — Rank archived entries by a keyword match.
python/reasoner/src/openral_reasoner/context.py
ContextRenderer builds the structured text snapshot the LLM consumes per tick (no pixels in v1).
- module constant
DEFAULT_BUFFER_SIZE: int = 8— Rolling buffer capacity per category. (L59) - module constant
DEFAULT_PROMPT_PRIORITY: int = 10(L64) — Default operator-prompt priority, matchingopenral_prompt_router.DEFAULT_SOURCES; human sources stamp 100 so they drain first. - module constant
_FAILURE_ADAPTER: TypeAdapter[FailureEvidence](L66) — cachedTypeAdapter(FailureEvidence)used to decode a failure buffer entry'sevidence_json. - module constant
_PERCEPTION_ADAPTER: TypeAdapter[PerceptionEventMetadata](L67) — cachedTypeAdapter(PerceptionEventMetadata)used to decode a perception buffer entry'smetadata_json. class FailureEventRecord(frozen dataclass) (L71) — Failure-buffer entry; fieldssource, kind, severity, evidence_json, rskill_id, trace_id, stamp_ns.class PerceptionEventRecord(frozen dataclass) (L88) — Perception-buffer entry; fieldskind, text, metadata_json, stamp_ns.class PromptRecord(frozen dataclass) (L98) — Operator-prompt-buffer entry; fieldstext, metadata_json, stamp_ns, priority=DEFAULT_PROMPT_PRIORITY.priorityis filled byappend_promptfrommetadata_json["priority"]when constructed with the default sentinel.render_robot_self_model(description: RobotDescription) -> str(L336) — Deterministic static self-model block (embodiment, dof, end-effectors, locomotion, payload, capability flags, cameras with FOV, control modes) so the LLM can judge reach/view feasibility before dispatch. Set viaContextRenderer.set_robot_model; rendered as## ROBOT.render_playbooks_block(entries: list[tuple[str, str]]) -> str(L297) — Renders the## PLAYBOOKSsystem-prompt block from(name—trigger, PLAYBOOK.md body)entries;""when none match. Playbooks guide decisions only — every motion still goes throughexecute_rskill+ the safety kernel.class MemoryEntry/class MemoryStore(openral_reasoner.memory, own section below) — The self-maintainedMEMORY.mdsemantic-memory model (complementary to the geometric scene graph), advisory only. Round-trips the file; renders a capped## MEMORYcontext block; applies explicit add/update/supersede/delete edits; consolidates duplicates; searches the archive. Loaded at configure byreasoner_node._maybe_load_memory.class ExecutionEventRecord(frozen dataclass) (L126) — Execution-feedback buffer entry; fieldsrskill_id, outcome ("ok"|"failed"), summary, reflection (failures only), stamp_ns.class RewardStateRecord(frozen dataclass) (L144) — Latest two-head reward assessment surfaced to the LLM:progress(closeness, the gated head),success(done-confidence, compressed, secondary),progress_trend,success_trend,task,stamp_ns. Rendered as## REWARDviaset_reward_state/_render_reward.reflect_on_failure(outcome_state, detail, *, timed_out: bool | None = None) -> str(L178) — Deterministic one-line strategy hint (no LLM call) turning a raw failure into a "change approach" cue.timed_outcomes from the typedExecuteRskill.Result.failure_kindwhen the caller has it;Nonefalls back to a deprecated substring probe over the prose.reflect_on_reward_plateau(progress_now) -> str(L231) — Reward-plateau hint (policy ran clean but progress says not done): the move is to change tactic, not to subdivide or re-issue the same instruction.reflect_on_invalid_plan(detail) -> str(L261) — Strategy hint feeding a decode error back so a model that emitted malformed JSON arguments fixes its call instead of re-issuing it verbatim.reflect_on_retry_cap(tool, cap) -> str(L285) — Strategy hint whenReasonerCore's per-kind retry ladder is exhausted fortool.class ContextRenderer(L404) — Stateful renderer.set_robot_model(self, robot_model) -> None(L497) — Sets/clears the static## ROBOTself-model; does NOT bumpseq.set_memory_block(self, memory_block) -> None(L506) — Sets/clears the## MEMORYblock from the MEMORY.md store; does NOT bumpseq.set_mission(self, mission) -> None(L547) — Sets/clears the active task queue rendered as## MISSION; a new goal is an event so it DOES bumpseq.set_spatial_memory(self, graph) -> None(L514) — Sets/clears the spatial-memory snapshot whose OBJECT nodes render asmemory_objects[<frame>]: <label> id=<node_id>@(x,y,z) size=(…)— the idsgrasp_target.object_idtakes; does NOT bumpseq(ingest jitter).set_in_view(self, objects) -> None(L559) — Sets/clears the latest continuous-detector enumeration rendered asin_view[<camera>]in## WORLD_STATE; bumpsseqonly when the rendered enumeration actually changes.set_inflight_skill(self, rskill_id, *, stamp_ns=0, state='running') -> None(L579) — Records (or clears) the in-flightexecute_rskillgoal with its phase ("dispatching"until accept, then"running"), rendered as the leadingin_flight:line in## EXECUTION; state changes bumpseq.inflight_skill(self) -> str | None(L598) —rskill_idof the goal currently in flight, orNone; also read byReasonerCore's heartbeat-idle gate.inflight_state(self) -> str | None(L603) — Phase of the in-flight goal ("dispatching"/"running"), orNone.note_located(self, objects) -> None(L610) — Folds open-vocablocate_in_viewhits into a stickylocated[<camera>]line (latest-wins, capped) that survives the continuous detector's per-frame clobber, keeping a mislabelled goal noun grounded for decompose/dispatch; bumpsseq.clear_located(self) -> None(L654) — Drops the sticky open-vocab grounding on a new operator goal; bumpsseqwhen non-empty.set_reward_state(self, reward) -> None(L668) — Sets/clears the two-head reward assessment rendered as## REWARD; bumpsseq.mission(self) -> MissionState | None(L697) — the activeMissionState, orNone; the node mutates it in place for non-waking bookkeeping (record_attempt/mark_verifying).advance_mission(self, *, done, verdict) -> TaskState | None(L707) —complete_active/abandon_activethe active task + activate the next, bumpsseq;Nonewhen the mission is finished or unset.append_failure(self, record) -> None(L729) — pushes a failure event onto the rolling buffer; bumpsseq.append_execution(self, record) -> None(L734) — Pushes a skill execution outcome onto the rolling buffer; bumpsseqso feedback wakes an idle heartbeat.clear_failures(self) -> None(L748) — drops stale failure/execution context after/openral/estop_clearedso the next prompt is not poisoned by a resolved e-stop.append_perception(self, record) -> None(L743) — pushes a perception event onto the rolling buffer; bumpsseq.append_prompt(self, record) -> None(L762) — priority-ordered insert; buffer-evicts the lowest-priority oldest entry on overflow; bumpsseq.render(self, *, world_state) -> str(L805) — Returns the deterministic text snapshot for one LLM tick.## WORLD_STATEcovers joint_state/ee_poses/battery/diagnostics plus a 3Dscene_objects[<frame>]line (one entry per instance — label, centre, size, track — never deduped by label; arepeated_labels:line flags labels a bare name cannot ground), thememory_objects[<frame>]node-id line, and a depth-free, pixel-spacein_view[<camera>]line (so grounding still works with RGB-only / no octomap);note_locatedhits render as a stickylocated[<camera>]line distinct fromin_view.## REWARD(when set) carries both reward heads distinctly labelled so the LLM never blurs progress (persist-vs-replan) with success (done-ness).failures(self) -> tuple[FailureEventRecord, ...](L1044) — snapshot of the failure buffer (oldest first).perception_events(self) -> tuple[PerceptionEventRecord, ...](L1054) — snapshot of the perception buffer (oldest first).executions(self) -> tuple[ExecutionEventRecord, ...](L1049) — snapshot of the execution-feedback buffer (oldest first).prompts(self) -> tuple[PromptRecord, ...](L1059) — snapshot of the prompt buffer (oldest first).seq(self) -> int(L1064) — monotonic mutation counterReasonerCoreuses to short-circuit a heartbeat tick when nothing has changed since the last successful tick.drain_prompts(self, *, seen=None) -> tuple[PromptRecord, ...](L1078) — Pull-once, priority-desc + arrival-asc order; does NOT bumpseq.seendrains only those records by identity, so a prompt that arrived mid-LLM-call and was never rendered survives for the next tick.- module constant
_PROMPT_EXCLUDED_FIELDS: frozenset[str](L1141) — evidence fields_summarise_evidence_jsondrops by role rather than length (currently{"joint_positions_rad"}, useless to a planner). - module constant
_PROMPT_FIELD_BUDGET: int = 48(L1147) — char backstop past which_summarise_evidence_jsondrops a field and discloses it as+<field>[<n>]. _summarise_evidence_json(payload) -> str— Decode the FailureEvidence union and produce a one-line, 120-char-truncated summary; a field named in_PROMPT_EXCLUDED_FIELDSor past the_PROMPT_FIELD_BUDGETbackstop is dropped and disclosed as+<field>[<n>]. Exclusion is by role, not length, so identity fields survive truncation instead of noise fields. (L1180)_extract_priority(metadata_json) -> int— Parse a top-levelpriorityfield out of a PromptStamped's metadata; returnsDEFAULT_PROMPT_PRIORITYon missing / malformed / non-int payload.
python/reasoner/src/openral_reasoner/core.py
ReasonerCore, the transport-agnostic orchestrator. The ROS-side reasoner_node wraps this with rclpy.
class ReasonerTickResult(frozen dataclass) (L120) — Tick outcome; fieldstool_call,error,elapsed_s,suppressed_reason("","min_interval","heartbeat_idle","mission_finished","retry_cap","palette_empty"),traceparent(Nonewhen no realTracerProvideris installed)._call_identity(call) -> str— Canonical retry-cap identity of a tool call: the full argument payload minusrationale(rephrasing rationale between identical retries must not dodge the cap), JSON-serialised with sorted keys.class PreparedTick(dataclass) (L86) — In-flight state of a phased tick betweenprepare_tickandfinish_tick: LLM inputs (context_text,palette,system_prompt) plus prepare-time snapshots (started,seq,prompts, an unattached OTelspan,renderer,force,tier). Theseq/promptssnapshots boundfinish_tickto only what the model actually saw.llm_s/prompt_tokensare the only fields the LLM phase writes back, so tick latency can be attributed to the provider vs. reasoner-side overhead.- module constant
_RETRY_CAP_EXEMPT_TOOLS: frozenset[str](L68) —{"recall_object", "resolve_place", "locate_in_view", "wait"}, the tool kinds transparent to the retry cap (seeReasonerCorebelow). class ReasonerCore(L150) — Orchestrator.retry_cap(self) -> int(L237) — the configured consecutive-identical-call cap (read-only).streak_tool(self) -> str(L242) — tool kind of the current consecutive-call streak (""when none).reset_kind_streak(self) -> None(L246) — reset the consecutive-call counter used by the retry-cap gate.tick(self, *, world_state, renderer, palette, force=False, tier='heartbeat') -> ReasonerTickResult(L257) — Synchronous composition of the phased API (prepare → off-thread LLM call → finish) so the executor is never starved. Enforces the min-interval and retry cap (search tools +waitare exempt); short-circuits on a finished mission or idle heartbeat.force=Truebypasses every gate but the retry cap. Wraps the tick in areasoner.tickOTel span.prepare_tick(self, *, world_state, renderer, palette, force=False, tier='heartbeat') -> PreparedTick | ReasonerTickResult(L318) — Phase 1 of a tick: run the suppression gates and render the context; must run on the owning/executor thread.run_prepared_llm(self, prep) -> ReasonerToolCall(L461) — Phase 2 of a tick: the blockingselect_toolround-trip; the ONLY phase safe off-thread.finish_tick(self, prep, *, call=None, error=None) -> ReasonerTickResult(L493) — Phase 3 of a tick: retry-cap ladder + bookkeeping + span close, back on the owning thread.
python/reasoner/src/openral_reasoner/critic_watchdog.py
Tier-C critic progress-stall / success watchdog — default decision core for the reserved /openral/failure/critic source. Pure logic, import-safe (no rclpy). Source-agnostic: any reward model emitting a higher-is-better scalar drives the same watchdog; the critic producer node routes samples through CriticWatchdogGroup and publishes to the failure bus on a fire. Fires on stall OR success, waking the reasoner promptly when an attempt is likely done.
class CriticWatchdog(L62) — Progress-stall / success state machine; raisesValueErroronstall_patience < 1ormin_delta < 0. Fires oneCriticEvidencein two mutually exclusive, one-shot-per-streak cases: success (score crosses threshold) or stall (stall_patienceconsecutive non-improving observations). Success takes precedence when both would fire on the same sample.critic_id(self) -> str(L131) — Identifier of the upstream critic stamped onto emitted evidence.threshold(self) -> float(L136) — Pass threshold; observations at or above it are recoveries.stall_patience(self) -> int(L141) — Consecutive stalled observations required to fire.min_delta(self) -> float(L146) — Minimum strict improvement over the running best to count as progress.observe(self, score) -> CriticEvidence | None(L150) — Feed one progress/critic score and decide whether to fire.reset(self) -> None(L220) — Clears running best, stall counter, and both latches (call on a reasoner context shift).class CriticWatchdogGroup(L234) — Multiplexer keying oneCriticWatchdogpercritic_idso several reward models share the/openral/failure/criticsource independently.stall_patience(self) -> int(L293) — Consecutive stalled observations each watchdog requires to fire.min_delta(self) -> float(L298) — Minimum strict improvement each watchdog counts as progress.observe(self, *, critic_id, score, threshold) -> CriticEvidence | None(L302) — Lazily creates a watchdog percritic_id(threshold bound on first sight) and delegates.known_critics(self) -> frozenset[str](L328) — Return thecritic_idset seen since construction / last reset.reset(self, critic_id=None) -> None(L332) — drop one critic's watchdog (rebinds its threshold) or all.
packages/openral_safety/openral_safety/supervisor_node.py
Day-1 Python safety envelope: candidate_action → safe_action pass-through with real per-control-mode envelope checks, the estop latch/reset pair, and the latched SafetyStatus topic. Reserves the node name and topic surface for the future C++ kernel; any addition of enforcement beyond this file requires safety-WG sign-off.
class SafetyPassthroughNode(LifecycleNode)(L151) — Owns/openral/candidate_action → /openral/safe_actionplus the estop latch/reset pair and theSafetyStatustopic.__init__(node_name="openral_safety") -> None(L172)on_configure(state) -> TransitionCallbackReturn(L235) — Opens the publishers, subscriptions, service, and diagnostics heartbeat.on_activate(state) -> TransitionCallbackReturn(L325)on_deactivate(state) -> TransitionCallbackReturn(L357)on_cleanup(state) -> TransitionCallbackReturn(L367)on_shutdown(state) -> TransitionCallbackReturn(L399)_on_candidate_action(msg) -> None(L405) — Subscribes/openral/candidate_action. Drops the candidate and re-fires/openral/estopon an envelope violation; drops silently while already latched; otherwise forwards unchanged on/openral/safe_action. Emits asafety.checkOTel span per candidate._envelope_violation(msg) -> tuple[str | None, str](L462) — Dispatches oncontrol_mode: joint modes get the position-limit check; Cartesian/twist/gripper modes each get their own bound check. Every bound parameter defaults to-1.0(no enforcement declared, skip)._handle_violation(msg, *, kind, reason) -> None(L723) — Drops the chunk, latches the estop, publishesstd_msgs/Emptyon/openral/estop, and updates the latchedSafetyStatus._on_external_estop(_msg) -> None(L752) — Subscribes/openral/estop(defense in depth): any external estop publication latches this node too, independent of its own checks._on_estop_reset(request, response) -> object(L779) — Exposes/openral/estop_reset(std_srvs/Trigger); clears the latch only onceestop_reset_cooldown_s(default 0.5 s) has elapsed since the last estop.SafetySupervisorNode(L863) — Back-compat alias ofSafetyPassthroughNode, not a separate skeleton.SAFETY_STATUS_TOPIC: str(L72) —/openral/safety_status, the latched current-safety-state topic (RELIABLE + TRANSIENT_LOCAL + KEEP_LAST=1), published alongside — never instead of —/openral/estop. Same contract the C++ kernel publishes.SAFETY_STATUS_HEARTBEAT_S: float(L79) — 1.0 s liveness refresh;header.stampis re-stamped at this cadence even when nothing changed, so a durable value can be told apart from a dead publisher's leftover state.main(args=None) -> int(L866) — Entry point forros2 run openral_safety supervisor_node.- module constant
_KERNEL_LABEL_PASSTHROUGH: str = "passthrough"(L57) —safety.kernelspan/log label this Python passthrough stamps (vs. the future C++ kernel's own label). - module constant
_WORKSPACE_VIOLATION_KINDS: frozenset[str](L96) —{"workspace", "gripper_range"}, envelope-violation kinds_envelope_violationcan return. - module constant
_RATE_VIOLATION_KINDS: frozenset[str](L97) —{"cartesian_step", "cartesian_step_rot", "ee_linear_speed", "ee_angular_speed", "base_linear_speed", "base_angular_speed"}, the rate/speed-bound violation kinds. - module constant
DEFAULT_ESTOP_RESET_COOLDOWN_S: float = 0.5(L85) — default cooldown_on_estop_resetenforces since the last estop before clearing the latch.
cpp/openral_safety_kernel/include/openral_safety_kernel/lifecycle_kernel.hpp
C++ (Layer 5). Node-side caps on the world-voxel freshness parameters (hazard log Entries 033/034).
kMaxWorldVoxelDeadlineMs = 2000.0,kMaxWorldVoxelDataAgeBudgetMs = 3000.0,kDefaultWorldVoxelDataAgeBudgetMs = 1500.0— Withworld_voxel_enabled,on_configurereturns FAILURE for aworld_voxel_deadline_msoutside (0, 2000] or aworld_voxel_data_age_budget_msoutside (0, 3000]; the budget defaults to 1500 and 0 does not disable it. Deliberate mirror ofDeployRuntime's caps, pinned bytests/unit/test_perception_caps_mirror.py.- Workspace contact params (#349) —
workspace_contact_enabled(default false; requiresgrasp_allowance_enabled),workspace_contact_links(each a collision link, non-empty),workspace_contact_max_speed_mpsandworkspace_contact_control_period_s(finite > 0),workspace_contact_max_lead_m(finite, > 0, ≤kMaxWorkspaceContactLeadM),workspace_contact_jaw_joints(each acollision_joint_namesentry; default empty),workspace_contact_brake_enabled(default false; requires the rule: a JOINT_POSITION chunk whose only reasons to stop are the measured hand speed and/or lead becomes a hold at the measured pose, jaws keep their command, no latch, WARNsafety.workspace_brake),workspace_contact_hold_max_consecutive(default 0; needs the brake: a command the relaxed check still refuses while the measured pose passes is held, up to that many chunks in a row, then the latched stop) — no default the node would use:on_configurereturns FAILURE otherwise and WARNs the rule (both bounds, the freshness window) when on. The node ingestsGraspDeclaration.workspace_regionnext to the grasp region (ingest_workspace_contact, same frame/measurement-age/backstop/stream-freshness rules — a retirement ends the grasp region, not the workspace,safety.workspace_region_armed/_dropped/_rejected,safety.workspace_contact_summaryon drop), sets the hand speed of a JOINT_POSITION candidate from the MEASURED velocity (measured_velocity_fresh(now_ns): every FK dof has a finite/joint_statesvelocity at mostcollision_state_deadline_msold —on_joint_statestores it per dof indq_meas_, a message naming a joint without one forgets it; thenworkspace_hand_speedof FK(q_meas + dq_meas · period) vs FK(q_meas), once per candidate, every row), else the commanded-speed fallback per row (workspace_hand_speed: max over hand links oflink_displacement_bound(prev row, row)/ period; row 0's previous row is, per commanded joint, its previously accepted command whileworkspace_command_fresh— held, due ≤kWorkspaceCommandFreshPeriodsperiods ago — else the measured state; the tick-partner check the same, partner joints at their committed targets) and the hand lead (workspace_hand_lead: max over hand links oflink_displacement_bound(measured, row)with the jaw joints held at measured, not divided by the period); unknown →+inffor every other mode or a stale measured state. Held commands are recorded on accept and forgotten (clear_workspace_commands) on any refused chunk, an external E-stop, a reset, configure/activate/cleanup, and a workspace armed/dropped. WARNssafety.workspace_contact(speed +speed_source=measured|commanded, and lead) ≤ 1 Hz per link, and addsworkspace_contact_active=/hand_speed_mps=/hand_lead_m=/hand_speed_source=tosafety.collision,safety.workspace_contact_active/_pairs/_max_depth_m/safety.workspace_hand_speed_mps/safety.workspace_hand_speed_source/safety.workspace_hand_lead_mspan attributes and max speed (+max_speed_source) + max lead tosafety.workspace_contact_summary. kMaxRegionMeasurementAgeS = 4.0(2 ×kMaxWorldVoxelDeadlineMs) — Cap ongrasp_region_max_age_s/place_region_max_age_s, how old a producer-measured region'sstamp_nsmay be and still exempt anything.0(default) resolves to 2 ×world_voxel_deadline_ms;on_configurereturns FAILURE for a resolved value outside (0, 4] while the matching allowance (grasp_allowance_enabled/attached_collision_enabled) is on. A stale or future-stamped region is dropped (reason=region_stale) at ingest and per candidate; not applied to a grasp box latched at handover.
cpp/openral_safety_kernel/include/openral_safety_kernel/collision.hpp
Allocation-free attached-payload contact handling, plus the staged 26-DOP → exact-convex-hull narrow phase for the arm-link-vs-world-voxel check (collision-hull-narrow-phase.md, collision-tight-geometry.md).
struct CollisionHit— One collision check's outcome and, on a hit, its E-stop evidence. Since #349 alsoworkspace_pairs/workspace_link/workspace_cell/workspace_min_distance: disclosure of the (hand link, cell) pairs within the margin the live workspace tolerated, never a decision.link_a/link_b/min_distancename the single deepest tripped pair;sweep_min_distanceis the separate sweep-wide minimum and never supplies the evidence distance.advisoryis the one field that is itself a decision — refuses the action without latching, for an attached payload only, never for a robot link.hull_hull_distance(a, b, margin, fallback, depth_is_box_bound = nullptr) -> double— Stage 2 for a self-collision pair: GJK on the two links' exact hulls, replacing the OBB SAT when both ship stage-2 geometry. The optional out-parameter reports whether the returned depth is the (looser but sound) box's bound rather than a true hull measurement — GJK proves an overlap but does not size it. Disclosure only; never gates the trip decision.check_contact_force_gate(attached) -> ContactForceGateResult— Declaration-scoped contact-force gate for a place, where geometric clearance is zero by design. Only ever adds a refusal, evaluated after every geometric check has passed. Arms only with a valid live declaration, a matching object/witness, and calibration.kMaxContactForceThresholdN = 140.0mirrors the schema's cap (ISO/TS 15066 Table A.2). Allocation-free.place_approach_allowance_cap(resolution) -> double— The place approach allowance's cap at a live voxelresolution:min(1.5 × voxel, 4 cm),0.0for an unusable resolution.constexpr, header-inline; the single definition the geometry and the log line both quote.place_approach_allowance(grid, object_index, center) -> double— Margin reduction the live place declaration grants this payload against this occupied cell, or0.0. Returns the reduction only when the region is valid, the object is inobject_mask, the box is finite/non-degenerate, and the cell centre is inside it (exact point-in-OBB). Reduces the margin only — deepening past it still stops, and arm-vs-world is untouched.enum class PlaceRegionStatus— Outcome of a place-region ingest:kOk,kNoObject(ordinary pre-grasp state),kBadPose(non-finite pose; the node also returns it for a wire quaternion not unit to 1e-6),kBadExtents,kDegenerate,kOversize,kOversizeVolume,kBadGeometry,kGeometryOverflow. Every non-kOkvalue means no allowance.ingest_place_region(pose, half_extents, object_mask, out) -> PlaceRegionStatus— Validate a producer-supplied region intoout, fail-closed toward no allowance (a bad region can only make the kernel more permissive). Rejects a non-finite pose/extent, an over-large side or volume, and an emptyobject_mask, each under its own status. Not on the hot path.ingest_place_target_geometry(geometry, store, region) -> PlaceRegionStatus— Lowers the declared target's own collision primitives into an already-validatedregion, so a payload can be adjudicated against the modelled receptacle instead of its voxel quantisation. Call only after the box validates; anything butkOkresets the whole region.storemust be sized tokMaxPlaceTargetPrimitives(64) and outlive every check reading the region. Rejects a malformed shape or an over-cap list — a half-modelled receptacle is worse than none. Not on the hot path.place_target_distance(region, prim, prim_xf) -> double— Exact surface distance from one attached payload primitive to the declared target's own geometry;+infinitywhen the declaration ships none, collapsing to the pre-existing voxel-only path. Allocation-free.place_region_status_reason(status) -> const char*— Stable snake_case token for aPlaceRegionStatus, used as thereason=key of the node's place-region log lines. Never null.support_contact_exempts(object, object_xf, center, resolution, slack) -> bool— Whether objecti's attested support contact explains this occupied cell: the cell must be inside the attested patch laterally and no higher above the plane than the voxel's own projected half-width plus attested depth, slack, and one voxel of co-planar headroom. Mirrored in lockstep bysupport_patch_withholds(03-world-state.md). Caller must have already established the witness is live.update_support_contact_witnesses(attached, scratch, grid, live_mask, margin) -> uint8— Refreshes the per-object support-witness latch against the measured configuration; a bit clears on separation and is never re-set here, so re-contact after a lift is a new violation. Returns 0 (fail-closed) when the map is unusable. Reads occupancy, so it is partitioned against the Layer-2 payload clearing that withholds the attested patch's cells precisely so this latch keeps something to measure.update_attached_voxel_contacts(attached, scratch, grid, contact_mask, contact_distance, mask_capacity, distance_capacity, snapshot) -> bool— Snapshots the occupied voxel contacts present at attachment and refreshes them against the measured payload pose; only that embedded subset (the payload's own uncleared occupancy residue) still exempts anything, no bits added after the snapshot.place_advisory_depth(resolution) -> double—min(0.2 × resolution, 5 mm), the depth past the place approach allowance within which an attached payload's world contact is refusable rather than latching.0.0for an unusable resolution, collapsing to today's latched stop.check_self_collision(model, scratch, margin, workspace = nullptr) -> CollisionHit— #349: with a liveWorkspaceContactRegion, a pair of two hand links (both in its mask, both link origins inside its box) does not trip while the hands are slow (workspace_link_slowon both); counted inworkspace_pairs, at its own depth insweep_min_distance.nullptr= the unchanged check.check_attached_self_collision(model, attached, scratch, margin, workspace = nullptr)applies the same rule to a carried payload on its attach link against another hand link.check_voxel_collision(model, scratch, grid, margin) -> CollisionHit— Checks every FK'd robot primitive against the occupied cells of a dense voxelgrid. Two narrowly scoped exemptions, both default off: the ADR-0115 grasp region (declared contact links, cells centred in the region) and, after it, the #349 manipulation workspace (hand links, cells centred in the confirmed workspace, while the hand is slow —workspace_contact_exempts); tolerated workspace pairs are counted in the hit. Never setsadvisory— a robot link against the world is always a latching stop. A boxed link with tight geometry runs a staged narrow phase (26-DOP, then exact hull GJK) that only tightens, never loosens, the reported distance versus the plain OBB. One narrowly scoped exemption: whilegrid.grasp_regionis valid, a mask link vs a cell centred in its box does not trip and reachessweep_min_distanceonly, at its own depth (the lifecycle's velocity band clamps an untripped check's slack to 0, so it reads as the slowest rate, never a negative slack that would hide the other pairs' graded slowdown) and never spends the stage-2 budget; every other link's trips are unchanged or, with the budget freed, fewer. Allocation-free.struct GraspTargetRegion/VoxelGrid::grasp_region— Producer-measured region of a live grasp declaration (ADR-0115 draft):valid,link_mask(std::bitset<kMaxGraspMaskLinks>, 256), base-framepose,half_extents, plus lifecycle bookkeepinghandover/object_indexthe geometry never reads. Invalid by default — no exemption anywhere; armed only by the lifecycle node withgrasp_allowance_enabled.kMaxGraspRegionHalfExtentM = 0.20,kMaxGraspRegionVolumeM3 = 0.03,kMaxGraspDeclarationTimeoutS = 120.0— Safety-WG placeholder caps on a grasp region and its declaration's backstop; past any of them it grants nothing.kGraspRetiredCapacity = 16/class RetiredGraspSet— The kernel's retired grasp-declaration identities (HZ-0115-3, multi-pick per goal): a fixed ring of (std::hashoftarget_id,stamp_ns);contains(hash, stamp),insert(hash, stamp) -> bool(true when full and the oldest was evicted; a held identity is a no-op),clear(),size(). No allocation (the lifecycle retires on the candidate path); a hash collision only refuses a fresh identity. Capacity is a Safety-WG placeholder.enum class GraspRegionStatus— Outcome of a grasp-region ingest:kOk,kNoLinks,kBadPose(non-finite pose; the node also returns it for a wire quaternion not unit to 1e-6),kBadExtents,kDegenerate,kOversize,kOversizeVolume. Every non-kOkvalue means no exemption.ingest_grasp_region(pose, half_extents, link_mask, out) -> GraspRegionStatus— Validate a grasp region intoout, fail-closed toward no exemption. Frame matching is the caller's job. Not on the hot path.grasp_target_exempts(grid, link_index, center) -> bool— Whether the live grasp region exempts this link from the occupied cell centred atcenter(base frame): valid region, link in the mask, finite non-degenerate box, centre inside it (the exact point-in-OBB testplace_approach_allowanceuses). Allocation-free.grasp_region_contains(region, p) -> bool— Whether base-frame pointpis inside a valid grasp region's box (the containment half ofgrasp_target_exempts); the lifecycle asks it of the declared payload's origin during the handover. Allocation-free.grasp_region_status_reason(status) -> const char*— Stable snake_case token for aGraspRegionStatus, for thereason=key of grasp-region log lines. Never null.struct WorkspaceContactRegion/VoxelGrid::workspace_region— Producer-confirmed manipulation workspace of a live grasp declaration (issue #349, draft, Safety-WG review pending):valid, hand-linklink_mask(std::bitset<kMaxGraspMaskLinks>), base-framepose,half_extents, plushand_speed_mps(the checked row's commanded hand speed bound, set by the lifecycle per row;+inf= unknown) andmax_speed_mps,hand_lead_m(the checked row's lead over the measured pose;+inf= unknown) andmax_lead_m. Invalid by default — no exemption; armed only by the lifecycle node withworkspace_contact_enabled.kMaxWorkspaceRegionHalfExtentM = 0.50,kMaxWorkspaceRegionVolumeM3 = 0.25— Safety-WG placeholder caps on a workspace (one tote interior or shelf slot); mirrorGraspDeclaration.MAX_WORKSPACE_HALF_EXTENT_M/MAX_WORKSPACE_VOLUME_M3.kMaxWorkspaceContactLeadM = 0.10— Safety-WG placeholder ceiling onworkspace_contact_max_lead_m; mirrors thele=0.10bound onDeployRuntime.workspace_contact_max_lead_m.kWorkspaceCommandFreshPeriods = 2.0— How many control periods a previously accepted JOINT_POSITION command stays row 0's previous row for the workspace hand speed (#349); past it, row 0 is measured against the measured state.workspace_contact_exempts(grid, link_index, center) -> bool— Whether the live workspace exempts this link from the occupied cell centred atcenter(base frame): valid workspace, link in the hand mask, a finite hand speed ≤ a finite positivemax_speed_mpsand a finite hand lead ≤ a finite positivemax_lead_m, finite non-degenerate box, centre inside it (exact point-in-OBB).check_voxel_collisionasks it only of pairs the ADR-0115 grasp region did not exempt (precedence), andcheck_attached_voxel_collisionof a carried payload's cells on its attach link after the support-witness and attach-residue exemptions, with the same accounting (no stage-2 budget, sweep minimum at its own depth, never the evidence). Allocation-free.ingest_workspace_region(pose, half_extents, link_mask, max_speed_mps, max_lead_m, out) -> GraspRegionStatus— Validate a confirmed workspace intoout(the grasp-region geometry checks against the workspace caps);hand_speed_mpsandhand_lead_mstart unknown. Frame matching is the caller's job. Not on the hot path.attached_object_reach(attached, object) -> double— Radius about a payload's attach-link origin bounding all its primitives (|primitive origin in the link frame| + box |half_extents| or capsule/sphere half_length + radius);+infon a malformed model. The #349 workspace speed bound uses it for a carried payload's attach link (the lead keeps the hand's own reach) (SafetyKernelLifecycleNode::workspace_reach).link_geometry_reach(model, link) -> double— Radius about a link's frame origin bounding all its collision geometry (capsules |origin| + half_length + radius, boxes |origin| + |half_extents|); configure-time.link_displacement_bound(from, to, reach) -> double— Upper bound on how far any point withinreachof a link origin moves between two poses: |Δt| + θ·reach (θ the relative rotation angle);+infon non-finite input. Allocation-free.kDopAxes/kDopAxis[kDopAxes][3]— The 26-DOP's 13 unit axis directions (3 face, 4 corner, 6 edge; each alo/hislab) in the owning OBB's local frame; the first three are the box's own axes. Mirrored in Python asopenral_core.schemas.DOP_AXES(14-duplication-watch.md item 12).kMaxTightHullVertices = 320— Hard ceiling on a stage-2 hull's vertex count, enforced at configure time; a cost bound (the exhaustive-scan support function is linear in vertex count), not a geometry choice. A hull over the cap runs stage 1 only. Mirrored asMAX_TIGHT_HULL_VERTICES.kTightContainmentEpsilonM = 1e-9— Floating-point slackvalidate_tight_geometryallows checking a stage-2 vertex against its own DOP slabs (tangent by construction). The DOP-inside-OBB check itself takes no slack. Mirrored asTIGHT_CONTAINMENT_EPSILON_M.kGjkMaxIterations = 24/kGjkTolerance = 1e-9— Iteration ceiling and convergence tolerance for the stage-2 GJK loop; hitting the ceiling is not a failure — every iteration's result is a lower bound, so a truncated run is simply more conservative.kMaxStage2PerCheck = 32— Ceiling on stage-2 invocations in onecheck_voxel_collisioncall across every link; past it the check keeps refining with stage 1 and the shippedbox_box_distance, so the cap can only make the answer more conservative. Exempt grasp-target pairs never spend it. Changing it changes a real-time bound — belongs in the hazard log.struct LinkHull— Tight convex geometry refining ONEObbinCollisionModel::boxes, in that box's local frame. Fields:dop_lo/dop_hi(stage 1, always on) andvertex_first/vertex_count(stage 2, CSR offset intohull_vertices;vertex_count == 0means stage 1 only).enum class JointKind/is_mimic(kind)—kFixed,kRevolute,kPrismatic, and since #349kMimicRevolute/kMimicPrismatic: a link posed from ANOTHER joint's column (dof_indexis the mimicked joint's) atmimic_multiplier * q + mimic_offset— the URDF<mimic>relation, the OpenArm's follower finger. No qpos slot, no Jacobian column (jacobian_dls_stepskips it; configure refuses one on the EE chain).docs/reference/finger-mimic-geometry.md.CollisionModel::mimic_multiplier/mimic_offset— Per link when the model has a mimic link (collision_mimic_*), else empty (identity relation for every link).CollisionModel::box_hull/hulls/hull_vertices— The threeCollisionModelmembers carrying tight geometry:box_hull(parallel toboxes) indexes intohullsfor the tight representation refining that box,-1for none. An emptybox_hullmeans no box carries tight geometry — the pre-existing behaviour every capsule-lowered robot keeps.AttachedPrimitive::hull_index/AttachedModel::hulls/AttachedModel::hull_vertices— The payload twin of the robot's tight-geometry fields, for the payload-vs-world-voxel check only. Unlike a robot's fixed geometry, a payload's hull arrives on every world-state message and is refilled byingest_attached_objectswithout allocating. Ingest fails DOWN, not closed: an unprovable refinement is dropped and the primitive checked as the plain box (refusing the whole attachment would drop its geometry — the unsafe direction). An over-budget hull is refused whole, never truncated.enum class TightGeometryStatus— Whyvalidate_tight_geometryrefused a model:kOk,kBadArity,kEscapesBox,kHullEscapesDop,kTooManyVertices,kDegenerate. Anything butkOkis fail-closed: the kernel keeps the shipped OBB narrow phase for every link.validate_tight_hull(hull, half_extents, vertices) -> TightGeometryStatus— The containment proof for one tight representation against one box: slabs finite,hull ⊆ 26-DOP ⊆ box, vertex count in budget, CSR slice in range. Shared by the robot's configure-time check and the payload's per-message ingest, so both are held to the same standard. Allocation-free.validate_tight_geometry(model, offending_box) -> TightGeometryStatus— Proves, at configure time, that every declared tight representation is a subset of the shipped OBB it refines, keepingcheck_voxel_collision's broad-phase reach correct.offending_boxreceives the offending index on refusal. Not on the hot path; allocation-free.tight_geometry_status_reason(status) -> const char*— Stable snake_case token naming aTightGeometryStatus, for the kernel's tight-geometry log line. Never null.struct TightPose/tight_pose_init(hull, hull_vertices, box_xf, half_side, out) -> void— Per-link-pose constants for the staged narrow phase, hoisted once per checked configuration outside the cell loop. Holds pointers into the caller'sCollisionModel; must not outlive it. Allocation-free.dop_cell_lower_bound(pose, center, half_side, best_axis) -> double— Stage 1: a conservative lower bound on the surface distance between the link's 26-DOP and an occupied voxel cube, via separating-axis over 16 directions.best_axis(nullable) receives the winning normal, seeding stage 2's search. Allocation-free.struct GjkWitness— Stage-2 warm-start carried between adjacent cells/steps. Stores hull vertex indices and cube-corner sign codes (hints, re-evaluated per cell), never cached Minkowski-difference points;n == 0is a cold start.hull_cell_distance(pose, center, half_side, seed_dir, margin, fallback, witness) -> double— Stage 2: a lower bound on the surface distance between the link's exact convex hull and an occupied voxel cube, seeded from stage 1's separating axis. Every returnable value is a supporting-hyperplane lower bound, so the iteration cap and early exit can only make the answer more conservative; on overlap,fallback(stage 1's figure) is returned instead. Allocation-free (fixed-size stack simplex).hull_hull_distance(a, b, margin, fallback) -> double— Stage 2 for a self-collision pair: the same GJK with the cube's corners replaced by a second hull's vertices.check_self_collisioncalls it for an OBB pair withinmarginwhen both links ship a stage-2 hull; the OBB stays the broad phase and fallback, so a manifest withouttight_geometryis bit-for-bit unchanged. Exists because the OBB alone cannot answer for two interleaving links whose real geometry only sometimes interpenetrates. Does not stop at the first bound clearingmargin(its result is the reportedsweep_min_distance). Cold-started. Allocation-free.
packages/openral_safety/openral_safety/envelope_loader.py
Pydantic → C++ kernel ROS-param bridge.
- module constant
_ACTUATED_JOINT_TYPES: frozenset[str](L185) —{"revolute", "prismatic", "continuous"}, joint types counted as actuated DOF. merge_deploy_envelope(robot_env, deploy) -> SafetyEnvelope(L355) — Apply explicitDeployScene.safetyfields to the robot ceiling with tighten-only validation; omitted fields keep robot manifest values.class EnvelopeIntersection(L61) — The numerical product ofrobot.safety ∩ skill.envelope.compute_intersection(robot, skill, *, deploy=None, gripper_convention=None) -> EnvelopeIntersection(L406) — Robot ceiling ∩ optional deploy/workcell envelope ∩ optional skill envelope; rejects (never clamps) any deploy or skill safety field that loosens the robot ceiling. Gripper channels come from each actuated end effector's command range, or fromgripper_convention(a simulated scene's encoding) when set.kernel_params_from_envelope(envelope) -> dict[str, object](L542) — Canonical scalar/AABB envelope → kernel ROS-param dict.- module constant
_JOINT_KIND_CODE: dict[JointType, int](L621) —{REVOLUTE: 1, CONTINUOUS: 1, PRISMATIC: 2}, the kernel's per-joint kind code used when flattening the kinematic chain. - module constant
_MIMIC_KIND_CODE: dict[JointType, int](L629) —{REVOLUTE: 3, CONTINUOUS: 3, PRISMATIC: 4}: aMimicJointedge's kernel kind, posed from the mimicked joint's column. collision_params_from_description(robot, *, margin_m=None) -> dict[str, object](L793) — Flatten collision geometry + ACM + the kinematic chain into the kernel's collision params. RaisesROSConfigErrorunless the links form one connected tree, and again if it cannot lower a primitive's shape, rather than silently mis-approximating it. A boxed link withtight_geometryalso lowers the staged-narrow-phase DOP/hull arrays. Amimic_jointsedge lowers tocollision_joint_kind3/4 on the mimicked joint's column plus the per-linkcollision_mimic_multiplier/collision_mimic_offsetarrays (#349).merge_extra_allowed_pairs(params, pairs) -> dict[str, object](L1007) — Additive deploy-scene ACM merge. Resolves link names, rejects unknown/self pairs, dedupes order-insensitively, no-ops when self-collision geometry is disabled.ee_link_index_from_collision_params(params) -> int(L1053) — Picks the predictive-Cartesian EE control link (the kinematically deepest collision link with no mimic joint on its chain — the kernel refuses a mimic EE) for the kernel's Jacobian look-ahead;-1when no collision model (predictive disabled, reactive floor only).
packages/openral_safety/openral_safety/mjcf_lowering.py
Offline MJCF → kernel collision-params lowering; imports mujoco lazily.
- module constant
_MJ_GEOM_PLANE: int = 0(L35) - module constant
_MJ_GEOM_SPHERE: int = 2(L36) - module constant
_MJ_GEOM_CAPSULE: int = 3(L37) - module constant
_MJ_GEOM_CYLINDER: int = 5(L38) - module constant
_MJ_GEOM_BOX: int = 6(L39) — MJCFgeom_typeenum valueslower_collision_paramsreads from the compiled model. - module constant
_LOWERABLE_GEOM_TYPES: frozenset[int](L80) —{sphere, capsule, cylinder, box}, the geom types this lowering path handles (mesh/plane skipped). lower_collision_params(model, joint_names, *, margin_m=0.0) -> dict[str, object](L270) — Lowers a compiledmujoco.MjModelto the kernel's collision params: per-link origins, every collidable primitive as a capsule,dof_indexassigned by movable-joint order (not by MJCF joint name, which can differ from the manifest), ACM = parent↔child + MJCF excludes + a neutral-pose overlap sweep.
packages/openral_safety/openral_safety/kernel_predicates.py
The Python mirror of the C++ kernel's narrow phase (cpp/openral_safety_kernel/src/collision.cpp), batched over a leading configuration axis. Anything offline that decides what the kernel will do must ask the same question with the same geometry. Every dispatch is exhaustive over CollisionShape and ends in a typed ROSConfigError, never a silent fallback.
- module constant
_DEGENERATE_AXIS: float = 1e-9(L68) — near-zero-length-axis tolerance used when normalizing a separating axis. - module constant
_TERNARY_ITERS: int = 48(L70) — ternary-search iteration count forbox_capsule_distance's convex point→segment minimization. box_box_distance(a_tf, a_half, b_tf, b_half) -> NDArray(L94) — 15-axis separating-axis surface gap between two oriented boxes (port of the C++ kernel); the max over the 15 candidates is the exact signed separation.<= 0means overlap.box_capsule_distance(box_tf, box_half, cap_tf, cap_radius, cap_half_length) -> NDArray(L145) — Box↔capsule gap: the capsule's segment ternary-searched against the box in its local frame, minus the radius.capsule_distance(a_tf, a_radius, a_half_length, b_tf, b_radius, b_hl) -> NDArray(L186) — Capsule↔capsule gap: segment-segment minimum distance minus both radii. A sphere is a capsule withhalf_length = 0.shape_distance(a_shape, a_tf, b_shape, b_tf) -> NDArray(L247) — Dispatch reproducing the kernel's self-collision routing (box↔box, box↔capsule either order, else capsule/sphere); the single entry point an offline sweep should use.shape_max_extent_m(shape) -> float(L288) — Farthest any point ofshapelies from its own origin; bounds how far a link's geometry sits from a joint axis.bounding_capsule_segment(shape, origin) -> tuple[p0, p1, radius](L303) — A capsule that containsshape, for consumers (today onlycumotion_config) that can only express capsules; a box takes its longest axis as the segment and the half-diagonal of the rest as the radius.eroded_shape(shape, eps_m) -> CollisionShape | None(L363) — Minkowski erosion by aneps_mball;Nonewhen empty, which a caller must read as "cannot certify", never as "no overlap".
packages/openral_safety/openral_safety/urdf_lowering.py
Offline URDF(+SRDF) → manifest collision-model lowering tool; lazy-imports yourdfpy / trimesh (the [lowering] group). Populates robot.yaml's collision geometry + ACM (the hand-reviewable manifest path), distinct from mjcf_lowering (the runtime MJCF path).
An ACM entry removes a self-collision check, so every rule here fails toward fewer entries. The always-colliding proof (_certified_always_colliding) is a branch-and-bound Lipschitz-bounded sweep over the pair's relative-DoF subspace whose cost knobs can only withhold an entry, never certify an unsound one.
parse_srdf_disabled_pairs(srdf_path) -> set[frozenset[str]](L81) — Parse a MoveIt SRDF's<disable_collisions>rows into unordered link pairs (the ACM).fit_capsule_to_vertices(vertices, *, headroom_m=CAPSULE_HEADROOM_M) -> tuple[CapsuleShape, tuple[float×6]](L245) — Minimum-volume bounding capsule containing every vertex: axis searched over the principal axes + a 256-direction hemisphere + local refinement, the line through the minimal enclosing circle of the projection, the segment the shortest that still holds every vertex; the radius then grows by the declaredheadroom_m. Containment holds for every candidate axis by construction. Returns the shape + link-frame origin.LinkFit(L472) — Frozen dataclass: one link's fittedprimitives((shape, origin)list), the winningkind(capsule/box/capsule×k),volume_ratio(union ÷ hull volume),max_protrusion_m/mean_protrusion_m(largest / mean support excess over the hull),candidates({kind: (volume_ratio, max_protrusion_m, mean_protrusion_m)}) andfitted(every candidate's primitives, for re-picking in a review).fit_link_primitives(mesh, *, headroom_m=CAPSULE_HEADROOM_M, max_capsules=3, force_box=False) -> LinkFit(L511) — The one fitting step both lowering paths call on a link'strimesh.Trimesh(collision + visual, link frame): fits one capsule (fit_capsule_to_vertices), one box (fit_obb_to_vertices) and capsule chains of 2..max_capsules(the mesh cut into equal slabs along the capsule axis, each slab fitted), every candidate holding every vertex with the headroom; picks the least mean protrusion (support excess over the convex hull, averaged over directions), ties within_PROTRUSION_TIE_FRACTIONgoing to the cheaper kernel kind (capsule < fewer capsules < box); excess volume and max protrusion are reported per candidate.force_boxfor a link that will carrytight_geometry.fit_link_with_tight_geometry(link_name, mesh, *, headroom_m=CAPSULE_HEADROOM_M) -> LinkCollisionGeometry(L602) — One link as a box (fit_obb_to_vertices, origin rounded and half-extents rounded up to the manifest's 4 dp) plus thetight_geometryderived in that rendered frame (tight_geometry.tight_geometry_for_box), somesh ⊆ hull ⊆ DOP ⊆ boxholds for the box the kernel loads. What--tight-linkand a manifest-refined link lower to.- module constant
_RENDER_DP: int = 4(L599) — the precisionfit_link_with_tight_geometryrounds the box to before deriving its refinement (mirrorsopenral_cli.collision._BLOCK_FLOAT_DP). _rendered(shape, origin) -> tuple[CollisionShape, origin](L648) — A primitive rounded to the manifest's 4 dp;_fit_linkreturns every fitted primitive this way so the ACM certificate and the regression test reason about the numbers the kernel loads, not an unrounded twin.- module constant
_SPLIT_GRID_M: float = 0.01(L672) — step of the cut-plane grid_split_tight_piecessearches along a link's long axis; cuts land on its multiples, so the planes are round numbers and stable under a CAD revision. _slab_hull_points(mesh, axis, grid) -> tuple[list[ndarray], list[ndarray]](L675) — Per grid band the hull vertices of the mesh vertices inside it, per plane the mesh edges' crossings — the point setslice_plane(cap=False)keeps, reduced band by band (hull(A ∪ B) = hull(hull(A) ∪ hull(B))) so the cut search is affordable on a 331 k-vertex CAD mesh._best_cuts(slab_volume, n, k) -> tuple[float, list[int]](L712) — Thek - 1ofngrid planes minimising the summedslab_volume(a, b)(plane-1= the link's start,n= its end), by a dynamic program over the additive objective: O(k·n²) slab evaluations against C(n, k-1) for trying every cut; ties keep the lower plane. Returns(total, ascending plane indices),total = infwhen every cover has a degenerate slab._split_tight_pieces(link_name, mesh, k) -> list[LinkCollisionGeometry](L743) — A link askslabs along its capsule axis, eachfit_link_with_tight_geometry(box + DOP + exact hull): the cut planes are the_SPLIT_GRID_Mgrid positions_best_cutspicks for the least total hull volume, so a re-lower reproduces them from the mesh alone; slabs are cut as_capsule_chaincuts, so each piece's hull holds its solid slab and the union holds the link. For a link one hull would swallow the space around (the OpenArm torso: foot plate, column, shoulder block — one hull is 5.6× the mesh and 0.12 m past the column). RaisesROSConfigErrorwhenkleaves no grid position or a slab is degenerate._as_tight_links(tight) -> dict[str, int](L827) —{link: pieces}from either spelling oftight_links(a mapping, or bare names = one piece each). RaisesROSConfigErrorfor a piece count below 1 (it would otherwise quietly mean "unrefined")._fit_link(link_name, mesh, tight_links) -> list[LinkCollisionGeometry](L840) — One link's entries:fit_link_with_tight_geometryfor a linktight_linksmaps to 1,_split_tight_piecesfork > 1, elsefit_link_primitivesat manifest precision; the step both readers share._tight_links_of(robot, extra) -> dict[str, int](L860) — Links to lower as box + hull with their piece count: every link the manifest already refines (count = its tight entries) plusextra, the larger count winning — sticky, socollision checkreproduces a refined or split manifest and dropping a refinement or a slab is a manifest edit._twin_link_meshes(robot, urdf_model, manifest_dir) -> dict[str, Any](L2416) — The robot's MJCF twin mesh geoms per manifest link, in the link frame, when every mapped twin body frame coincides with its URDF link frame at zero (_TWIN_FRAME_TOL = 1e-6);lower_robotfolds them into the fit so the primitives hold the simulated robot's CAD too (the menagerie H1's forearm is 46 mm longer than h1_description's). Empty (and logged) when the frames differ,mujocois missing or the robot has no twin.- module constant
_PROTRUSION_TIE_FRACTION: float = 0.03(L436),_PROTRUSION_DIRS: int = 1024(L434),_UNION_VOLUME_SAMPLES: int = 60_000(L414) —fit_link_primitives's tie window, support-excess direction count and fixed-seed Monte-Carlo sample count for a chain's union volume. CAPSULE_HEADROOM_M = 0.001(L131) — declared clearance every URDF-lowered capsule keeps around its link's geometry; absorbs the manifest's 4-dp rendering (tests/unit/test_collision_geometry_enclosure_urdf.py).lower_link_geometry(urdf_path, *, tight_links=(), extra_meshes=None) -> list[LinkCollisionGeometry](L942) — The primitive set of every URDF link with a<collision>, fitted byfit_link_primitivesto the link's<collision>and<visual>geometry together (box→8 corners, cylinder/sphere→circumscribing polytope, mesh→trimesh); a lone sphere<collision>with no visual stays an exact sphere. A link may yield several entries (a capsule chain); a link intight_links(names, or{link: pieces}) lowers tofit_link_with_tight_geometryinstead, inkslabs when mapped tok > 1. RaisesROSConfigErrorwhen a link's geometry resolves only partially; skips a link none of whose geometry resolves.- module constant
_CERTIFY_MAX_DOF: int = 5(L1137) - module constant
_CERTIFY_MAX_CELLS: int = 400_000(L1141) - module constant
_CERTIFY_MAX_LEVELS: int = 96(L1144) - module constant
_COARSE_NODES: int = 7(L1146) — the four_certified_always_collidingcost knobs (see the module intro); hitting any withholds the ACM entry, never soundness. - module constant
_MJCF_RNG_SEED: int = 20260610(L1154) - module constant
_MJCF_N_SAMPLES: int = 2000(L1155) — defaults forlower_robot_from_mjcf's seeded always-colliding sweep. acm_for_geometry(urdf_path, geoms, *, srdf_path=None, margin_m=0.0) -> set[frozenset[str]](L1486) — The ACM for the exact primitives the kernel will load (a flat list, or a mapping by link to one primitive or its list), decided with the kernel's own predicates at the robot's own margin, each link pair's gap folded over its primitives.ACM = adjacent ∪ always-colliding ∪ [SRDF-disabled if given]— geometry can prove a pair always collides but never that it never does, so without an SRDF a sometimes-colliding pair stays checked. Deterministic, no RNG.sample_acm_from_urdf(urdf_path, *, margin_m=0.0) -> set[frozenset[str]](L1552) — No-SRDF fallback:adjacent ∪ always-collidingfrom the URDF's own geometry. Conservative against a precise-mesh SRDF (its disabled set is a subset, never false-permissive). The name is now a misnomer (nothing is sampled) but is kept as public API/provenance value._static_mesh_filename_handler(urdf_path) -> handler(L1635) —_rd_mesh_filename_handlerpluspackage://refs throughopenral_core.assets.resolve_package_uri, raisingROSConfigErroron one that does not resolve; for STATIC links only, never installed in_load_urdf(resolving the openarm arm meshes would flipselect_loweringoff the MJCF path)._static_link_meshes(robot, link_body, manifest_dir) -> dict[str, Trimesh](L1660) — Geometry of every static link — afixed_attachmentschild the MJCF has no body for (the OpenArm torso) — from the URDF's<collision>and<visual>elements, in the link frame. Fails closed: a static link absent from the URDF or with geometry that does not all load raisesROSConfigError; one with no<collision>is skipped with a warning; a robot with no static link returns{}without opening the URDF._fixed_link_world(robot, link_body, body_tf) -> dict[str, ndarray](L1736) — World pose of every linkfixed_attachmentsreach from a mapped MJCF body, upward (T_parent = T_child · origin⁻¹) and downward to a fixpoint, so a static link under the chain's root is placed from the mounts the MJCF carries instead of assuming the root is MuJoCo's world; two routes disagreeing by more than_TWIN_FRAME_TOLraiseROSConfigError.class LoweredCollisionModel(L1581) — Frozen dataclass result:collision_geometry(several entries per link allowed),allowed_collision_pairs(sorted tuples),acm_source("srdf"|"sampling"|"mjcf"),srdf_path,joint_fk.lower_joint_fk(robot, urdf_ref) -> dict[str, tuple[xyz, rpy, axis]](L1806) — Per-manifest-joint forward kinematics read from the URDF, matched bychild_link; needed to place the link capsules. Unmatched (synthetic) joints are omitted.lower_robot_from_mjcf(robot, *, n_samples=2000, seed=20260610, margin_m=0.0, manifest_dir=None, acm_only=False, tight_links=None) -> LoweredCollisionModel(L2201) — MJCF backend for robots with no URDF collision geometry (e.g. bimanualopenarm): fits the geometry withfit_collision_geometry_from_mjcf(kept from the manifest underacm_only, aslower_robotkeeps it), lowers joint FK from mujoco, and derives a conservative ACM from a seeded mujoco-FK overlap sweep over that geometry, folding each link pair's gap over its primitives as the kernel does, and never auto-disabling a pair whose two links both carry a hull (the kernel re-asks those of the hulls) — the one remaining sampled criterion, since the URDF joint-tree proof doesn't apply here. A static link (_static_link_meshes) is re-placed by_fixed_link_worldat every sample (it follows its parent, which may move), is adjacent to itsfixed_attachmentsparent, and is in scope so an SRDF row naming it survives. A manifestassets.srdfis unioned into the sweep's disabled set for hand-reviewed exemptions the sweep can't prove; hand-editing the generated ACM block is forbidden.acm_source="mjcf".- module constant
_MOVABLE_JOINT_TYPES: tuple[str, ...](L1253) —("revolute", "continuous", "prismatic"), joint typeslower_joint_fk/acm_for_geometrytreat as movable. lower_robot(robot, *, srdf_path=None, acm_only=False, geometry_only=False, manifest_dir=None, tight_links=None) -> LoweredCollisionModel(L2509) — Top-level entry; threadsrobot.safety.self_collision_margin_mintoacm_for_geometryso the ACM matches the kernel's live threshold. Geometry is scoped to the joint links plus everyfixed_attachmentslink, so a URDF body link the kernel should see is onefixed_attachmentsrow away. ACM source precedence: explicitsrdf_path→ manifestassets.srdf→ URDF-only fallback. RaisesROSConfigErroron zero fitted links, never an empty geometry/ACM the kernel would silently not check.select_lowering(robot, *, manifest_dir=None) -> LoweringSource(L2685) — Provenance-correct routing:"srdf"(SRDF + usable URDF geometry),"sampling"(URDF geometry, no SRDF),"mjcf"(no usable URDF geometry but an MJCF exists, e.g.openarm's unresolved package mesh refs). RaisesROSConfigErrorwhen no lowerable asset exists.lower_robot_auto(robot, *, acm_only=False, geometry_only=False, manifest_dir=None, tight_links=None) -> LoweredCollisionModel(L2730) — Single dispatch overselect_lowering→lower_robotorlower_robot_from_mjcf(acm_onlykeeps the manifest geometry on both paths;tight_linksadds box + hull links on top of those the manifest already refines,_tight_links_of); the one entry point the CLI and the regression test both call, so routing can never diverge. RaisesROSConfigErrorwhen atight_linksname matches no lowered link (a typo must not leave a link silently unrefined), and whentight_linksis combined withacm_only(which keeps the manifest geometry, so the request would be dropped silently).fit_obb_to_vertices(vertices, *, headroom_m=CAPSULE_HEADROOM_M) -> tuple[BoxShape, tuple[float×6]](L344) — Minimum-volume oriented bounding box (trimesh.bounds.oriented_boundsover the hull), half-extents re-measured from the cloud in that frame and grown byheadroom_m; every vertex inside by construction.- module constant
_MJCF_STROKE_SAMPLES: int = 9(L1161) — leader positionsfit_collision_geometry_from_mjcfsweeps a coupled follower over. mjcf_coupled_joints(model) -> dict[int, list[tuple[int, float, float]]](L1961) — MuJoCo joint equalities (q[obj1] = c0 + c1·q[obj2]) as a two-way map, joint id →[(other joint, c0, c1)], so a follower is set correctly whichever side the manifest drives (OpenArm drivesobj1). RaisesROSConfigErroron a non-linear or non-invertible coupling._declared_mimic_followers(robot, model, followers) -> set[int](L2041) — The MJCF follower joints the manifest models asmimic_joints, each checked to be an equality follower of itsmimicsjoint with the SAME multiplier/offset (ROSConfigErrorotherwise);fit_collision_geometry_from_mjcfleaves them out of the leader's sweep and fits them as their own rigid links.fit_collision_geometry_from_mjcf(robot, *, manifest_dir=None, stroke_samples=9, tight_links=()) -> list[LinkCollisionGeometry](L2089) —fit_link_primitivesper manifest link over every geom of its MJCF body — collision and visual, meshes and primitives — in the body frame. An equality-coupled follower body (OpenArm's second finger) is folded into its leader's child link, swept overstroke_samplesleader positions, so the primitives hold the gripper at every opening. A static link (afixed_attachmentschild with no MJCF body) is fitted from the URDF via_static_link_mesheswith the same fitter andtight_linksrule. Pinned bytests/unit/test_collision_geometry_enclosure.py. A follower the manifest declares inmimic_jointsis not folded in: it is its own rigid link, fitted in its own body frame (#349).LoweringSource—Literal["srdf", "sampling", "mjcf"]; the sourceselect_loweringresolves to (matchesLoweredCollisionModel.acm_source).
packages/openral_safety/openral_safety/static_geometry.py
Declared static geometry → base-frame surface points for the octomap bridge (#349, docs/reference/manipulation-workspace-contact.md §3.1).
load_static_geometry(path) -> tuple[StaticGeometry, Path](L34) — Load + validate aStaticGeometryYAML; returns it and its directory;ROSConfigErroron a missing / non-YAML / invalid file.surface_points(geometry, base_dir, spacing_m) -> NDArray(L76) — Every declared surface (box faces, mesh triangles) sampled at mostspacing_mapart,(N, 3)base frame; each surface point lies withinspacing_mof a sample.ROSConfigErroron spacing ≤ 0 or a missing/malformed mesh.write_points_file(points, path) -> None(L129) — The octomap bridge'sstatic_points_fileformat: onex y zper line,#header.
packages/openral_safety/openral_safety/tight_geometry.py
Derive a box's tight_geometry (26-DOP + exact hull) from the geometry it bounds; containment is definitional (mesh ⊆ hull ⊆ DOP ⊆ box). Moved here from tools/generate_tight_geometry.py so the collision lowering can refine links; the tool re-exports the moved names.
derive_tight_geometry(points, half_extents) -> dict[str, Any](L269) — The DOP slabs and the exact hull, refined toward the hull under theMAX_TIGHT_HULL_VERTICESbudget (refine_dop_to_budget) when it is over; a mapping forTightCollisionGeometryplus reviewer diagnostics.refine_dop_to_budget(points, dop_lo, dop_hi, budget) -> Points(L187) — A ≤budget-vertex envelope built only from tangent planes ofconv(points), starting from the DOP; refuses to emit one that cuts the points.hull_overhang_m(hull_points, mesh_points, mesh_faces, *, samples_per_edge=24) -> float(L106) — Sampled distance from the hull's facets to the real mesh surface (exact per sample, brute force, batched).hull_overhang_upper_bound_m(hull_points, mesh, *, samples_per_edge=24) -> float(L156) — The same measurement from above via a KD-tree over a fixed-seed surface sample, for CAD meshes too large for the brute-force query.tight_geometry_for_box(mesh, half_extents) -> TightCollisionGeometry(L309) — DOP + hull + padded overhang for a box-frame mesh, written at 1 nm (slabs rounded outward) so a re-lowering reproduces it byte for byte.round_up_m(value, precision_m=1e-6) -> float(L68) — Round a sampled distance up, never down.- module constant
HULL_OVERHANG_SAFETY_MARGIN: float = 1.2(L65) — pad on a sampled overhang;_OVERHANG_BATCH,_OVERHANG_MAX_SAMPLES,_SURFACE_SAMPLES,_SURFACE_SEED,_WRITE_DP— the query's memory bound, sample caps, seed and write precision.
packages/openral_safety/openral_safety/cumotion_config.py
Derive a cuRobo (cuMotion) robot-config from the lowered collision geometry. Pure module; uses kernel_predicates.bounding_capsule_segment. Deliberately over-covers the kernel's geometry — cuRobo can only express spheres, and a planner that believes the arm is thinner than it is emits trajectories the kernel then has to E-stop. Looser than the kernel is fine; tighter is not.
- module constant
_ACTUATED_JOINT_TYPES: frozenset[JointType](L50) —{REVOLUTE, PRISMATIC, CONTINUOUS}, joint typesactuated_joint_namescounts as single-DOF movable. class CuMotionSphere(L56) — Frozen dataclass:center(link-frame(x, y, z)),radius— one cuRobo collision sphere.capsule_to_spheres(p0, p1, radius, *, count) -> list[CuMotionSphere](L68) — Samplecountspheres evenly along the segmentp0→p1(endpoints inclusive forcount >= 2; midpoint forcount == 1).spheres_for_capsule(shape) -> int(L110) — Sphere count to tile a lowered primitive with centres ≤ one radius apart;1for a sphere / zero-length capsule. Also accepts aBoxShape.link_collision_spheres(geom, *, count=None) -> list[CuMotionSphere](L134) — Lower oneLinkCollisionGeometry(capsule, sphere, or box) to cuRobo spheres in its link frame, via the containing capsule.sphere_model_geometry(geoms) -> dict[str, list[LinkCollisionGeometry]](L156) — Each link's primitives as the capsules cuRobo's spheres actually cover (box → containing capsule; capsules/spheres unchanged) — a strictly larger solid than the kernel's box, so it needs its own ACM proof.actuated_joint_names(robot) -> list[str](L206) — Single-DOF movable joint names, in manifest order — the cuRobocspace.joint_names.- module constant
_SPHERE_DP: int = 6(L215) — decimal places sphere centers/radii are rounded to when rendered into the YAML fragment. render_cumotion_config(robot, model, *, urdf_path=None) -> str(L218) — Render a cuRoborobot_cfgYAML fragment with a generated-provenance header. Spheres come fromrobot.collision_geometry(falling back tomodel.collision_geometryonly when the manifest carries none). The ACM is not copied verbatim: givenurdf_pathit is widened with pairs proven always-colliding for the sphere model, so the planner stays looser than the kernel, never tighter. Withouturdf_paththe manifest ACM is used as-is and the header says so.
packages/openral_reasoner_ros/openral_reasoner_ros/reasoner_node.py
reasoner_node lifecycle wrapper. Thin rclpy shell around openral_reasoner.ReasonerCore.
- module constants
_FAILURE_SOURCES,_PERCEPTION_KINDS— closed sets:hal/sensor/rskill/safety/wam/criticandmotion/objects/ocr/scene_change. (L273) - module constant
_PERCEPTION_KINDS: tuple[str, ...](L274) —("motion", "objects", "ocr", "scene_change"). - module constant
_QOS_WORLD_STATE(L216) —KEEP_LAST=1, RELIABLE, VOLATILE, the/openral/world_state_slowsubscription QoS. - module constant
_QOS_FAILURE(L222) —KEEP_LAST=50, RELIABLE, VOLATILE, the six/openral/failure/*subscription QoS. - module constant
_QOS_PERCEPTION(L228) —KEEP_LAST=10, BEST_EFFORT, VOLATILE, the four perception-topic subscription QoS. - module constant
_QOS_PROMPT(L234) —KEEP_LAST=10, RELIABLE, VOLATILE, the/openral/promptpublisher/subscription QoS. - module constant
_QOS_MAP(L252) —KEEP_LAST=1, RELIABLE, TRANSIENT_LOCAL, the latched/mapsubscription QoS. - module constant
_QOS_COMPLETION_CAMERA(L263) —KEEP_LAST=1, BEST_EFFORT, VOLATILE, the VLM completion-camera subscription QoS. - module constants
_KIND_TIMEOUT,_KIND_CONTROLLER,_SEVERITY_WARN,_SEVERITY_FAIL— IDL-mirror constants foropenral_msgs/FailureTrigger, kept inline (not thefailure_bushelper) so the reasoner emits without pulling the rate-limiter into the dispatch path. (L300) - module constant
_KIND_CONTROLLER: int = 5(L301) - module constant
_SEVERITY_WARN: int = 1(L302) - module constant
_SEVERITY_FAIL: int = 2(L303) - module constant
_SKILL_FAILURE_KIND_NAMES: dict[int, str](L309) —{_KIND_TIMEOUT: "timeout", _KIND_CONTROLLER: "controller"}, kind-derived failure-state names for_emit_skill_failure_event. - module constants
_EXECUTE_SKILL_SERVER_PROBE_S,_LIFECYCLE_SERVER_PROBE_S— 100 mswait_for_server/wait_for_serviceprobes so an absent server/peer can't block the executor thread. (L320) - module constant
_LIFECYCLE_SERVER_PROBE_S: float = 0.1(L322) - module constant
_MISSION_VERIFY_WINDOW_S: float = 40.0(L283) — fallback verify window when the activeRewardContractsets noframe_window_s. - module constant
_DEFAULT_SUCCESS_THRESHOLD: float = 0.8(L292) — fallback reward-gatesuccess_thresholdwhen noRewardContractis loaded. - module constant
_DEFAULT_CHECK_FLOOR: float = 0.5(L293) — fallback reward-gatecheck_floorwhen noRewardContractis loaded. - module constant
_FAILURE_TIER_FOR_SOURCE: dict[str, str](L330) — trigger taxonomy mapping each/openral/failure/<source>to its tier:safety → "A",hal/sensor/rskill/wam → "B",critic → "C". Used to stampreasoner.tieron the OTel span; the preemption threshold itself is decided inline in_on_failure. - (the
hal_mode == "sim"gate imports the canonicalopenral_core.SIM_EXECUTABLE_CONTROL_MODES— trimmed to the six packer-implemented modes, see the Layer-0 core entry — rather than a module-local frozenset.) def _required_control_modes(manifest: RSkillManifest) -> set[ControlMode](L382) — Pure helper for the deploy-path palette gate. Readsaction_contractby specificity, fromNone(no constraint) down through representation/slots/legacydim.def _action_executable(manifest: RSkillManifest, description: RobotDescription, hal_mode: str) -> bool(L407) — Pure helper:Truewhen every_required_control_modes(manifest)is in the executable set forhal_mode(sim's fixed set, else the robot's declared modes). Empty required set →True.def _resets_search_episode(call) -> bool(L445) — Pure helper for the find→re-prompt cascade bound.Truewhen dispatchingcallends the active-search episode;Falsefor the three read-only search actions, so a search loop's own budget can never be reset by its own query and fail to terminate.class ReasonerNode(LifecycleNode)(L616) — Lifecycle node wrappingReasonerCore. Optional injectedspatial_memoryenables the recall/resolve tools; otherwisespatial_memory_path/spatial_memory_ingestparams can load or build one.memory_md_pathenables the memory tools.hal_modeselects the action-mode palette gate;tick_hz(default 0.2 Hz) is the heartbeat rate.- Grasp/place target grounding (real pick-and-place design §2.2-2.3). Params
grasp_target_voxel_m(0.0 = unset: no standalone default — on a manifest with arole: gripperjoint an unset value failson_configurewithROSConfigError, once, instead of a per-callROSReasonerInvalidPlanthe LLM would burn its replanning ladder on, so no cell-specific fallback pads it; a gripperless manifest needs none;deploy_e2epasses_octomap_resolution(hal_mode), the resolution octomap_server gets) andgrasp_target_extrinsic_error_m(MAX_PLANAR_ERR_M, 0.015) — their sum pads a grounded seed box._load_grounding_context()loads the manifest at the start ofon_configure(a load failure warns and a named target is then refused; a gripper manifest without the voxel raisesROSConfigError→ configure FAILURE); no robot unit and no cell geometry are read._ground_targets(call) -> (grasp, place)runs in_dispatch_execute_rskillbefore any latch:openral_reasoner.groundingover the latest/openral/world_state_slowdetected_objects(bbox now on the wire) and the spatial-memory scene graph; aROSReasonerInvalidPlanis logged, recorded as afailedexecution with a disambiguation hint, and NO goal is sent._send_execute_rskill_goalfillsgrasp_declaration_valid/grasp_declarationandplace_declaration_valid/place_declarationfor the matching dispatch generation; the runner stamps, attributes and strips any region as before. _submit_client_warmup(client) -> None— Aton_configure, kicks a managed LLM sidecar's boot onto_llm_poolinstead of leaving it to the first tick, off the executor thread so the lifecycle transition returns promptly. No-op for clients withoutwarm(); a failure here only warns, sinceselect_tool's lazy path still owns whether the server is usable.on_configure(self, state) -> TransitionCallbackReturn(L1079) — BuildToolUseClientfrom env if not injected, attach subscribers to world-state/failure/perception/prompt/registry topics, create the prompt/failure publishers +execute_rskillaction client. Readsvram_lifecycle_peers(GPU peers auto-deactivated around eachexecute_rskill), loads the reward manifest, and probes GPU total VRAM for the pre-dispatch fit check.on_activate(self, state) -> TransitionCallbackReturn(L1478) — Arm the periodic tick timer attick_hz.on_deactivate(self, state) -> TransitionCallbackReturn(L1486) — Cancel the tick timer (subscriptions remain attached).on_cleanup(self, state) -> TransitionCallbackReturn(L1505) — Tear down pending skill-goal deadline timers, destroy the action client and every cached per-topic emit_prompt publisher, drop cached lifecycle/service clients, clear the in-flight goal / cancel-reason / tick-trampoline state, and reap the LLM/VLM worker pools and inbox guard.on_shutdown(self, state) -> TransitionCallbackReturn(L1602) — Final shutdown.destroy_node(self) -> None(L1585) — Reaps thereasoner-llm/reasoner-vlmworker pools (no wait; an in-flight provider call is not interruptible) and detaches + destroys the executor-inbox guard condition beforeLifecycleNode.destroy_node, so a node destroyed without acleanup/shutdowntransition drops a late worker completion instead of triggering a destroyed guard (#381). Idempotent withon_cleanup._on_failure(source, msg)— Append aFailureEventRecordto the renderer and preempt the next tick per the tier taxonomy: Tier A (safety) atseverity ≥ WARN, Tier B/C atseverity ≥ FAIL. When acriticFAIL is a reward wake and anexecute_rskillgoal is in flight, cancels that goal (latched reason"reward") instead of ticking — the reward signal stops the VLA now, not at thedeadline_sclock — and the canceled result re-enters mission verification._on_tick(*, force=False, tier="heartbeat")— Single-flight trampoline over_start_tick: a tick requested mid-dispatch is coalesced (forcewins) and replayed after, bounded by_MAX_TICK_REPLAYS=4. Runs the blocking LLM call on a single-worker pool so the rclpy executor is never starved, then marshals dispatch back onto the executor. A round-trip landing after deactivate/cleanup is dropped by a generation check.- Mission lifecycle.
_on_promptseeds aMissionStatefrom the operator goal, gated byshould_rebuild_mission(cascades never rebuild; an in-progress mission needs explicitnew_goalmetadata)._dispatch_execute_rskillrecords the attempt and the accepted goal handle a reward wake can cancel, withdeadline_sset to the resolved patience ceiling. On skill return,_maybe_verify_active_mission_taskissues a windowedquery_task_progressand_on_mission_verify_responseapplies the three-tier verdict (auto-pass / vlm_check / ladder), gated on the progress head; an inconclusivevlm_checkfalls back into the retry ladder rather than retrying forever._emit_mission_completesummarizes a finished queue. - VLM completion adjudication.
completion_camera_topicparam (default""= disabled;deploy_e2epassescamera_topic(_primary_rgb_camera(...)), ADR-0108) subscribes and caches each frame as JPEG._adjudicate_completion(task_text) -> bool | Noneasks the tool-use client'sdescribe_image;None(no frame/client/provider error) degrades to the replanning ladder rather than a false verdict._complete_active_and_advanceis the shared advance path for both the native-complete and VLM-confirmed branches. - Task subdivision on replan.
_should_offer_subdivision(active, offered, max_depth) -> booldecides whether an abandoned task gets one decomposition offer, bounded by a one-offer-per-task set and the max depth, so a task that declines still terminates in human-handoff._dispatch_decompose_missionflat-splices finer children into a target task, or replaces the whole queue only when it hasn't started yet. No actuation. - Per-task locate budget.
_charge_task_locate_budget(call, *, traceparent) -> bool(top of_dispatch_locate_in_view) charges one locate cycle against the active task; onceDEFAULT_MAX_TASK_LOCATE_ATTEMPTSis exhausted without anexecute_rskilldispatch it abandons the subtask with a specific reason and forces a Tier-C tick, without dispatching the locate. Fixes a live locate-loop theSearchProgressmiss budget can't bound (that budget resets on a hit). Cleared on a new operator goal and on a realexecute_rskilldispatch. _on_skill_registry_changed(msg)— Palette refresh: walks installed rSkills, loads each manifest, rebuilds the palette against the active capabilities + commercial flag (preserving every availability flag, includingmemory_available), installs it viaset_palette._dispatch(call, *, traceparent=None)— Routing-only switch over theReasonerToolCallvariants; delegates to the per-tool_dispatch_*methods.WaitToolis a deliberate no-op;ReloadGstPipelineToolis a log-and-acknowledge stub pending its service IDL._dispatch_emit_prompt(call, *, traceparent)— Publish aPromptStampedoncall.target_topic(per-topic publisher cache;/openral/promptreuses the standing cascade publisher); stamps the threaded-throughtraceparentintometadata_json._dispatch_spatial_query(call, *, traceparent)— Read-only: runs aRecallObjectTool/ResolvePlaceToolagainst the injectedSpatialMemoryand republishes the result as a cascade prompt. Bounded by aSearchProgress/SearchBudget; exhausting it abandons the active mission task so later heartbeats cannot restart a terminal search. Arecall_objectmiss escalates policy-driven to a livelocate_in_viewfor the same term when a detector is available and the term hasn't already been escalated this streak. With a latched/map, every approach viewpoint is refined through the occupancy grid before rendering (BLOCKED when none exists)._maybe_load_spatial_memory()— Deployment wiring: onon_configure, when no backend was injected andspatial_memory_pathis set, loads the persisted scene graph and flipsspatial_memory_available. Load failure degrades to a WARNING + no backend, never a fabricated map._maybe_load_memory()— Deployment wiring: onon_configure, whenmemory_md_pathis set, parsesMEMORY.md, renders the## MEMORYblock, loads the archive log, and flipsmemory_available. Read failure degrades to a WARNING + empty store._dispatch_memory_write(call, *, traceparent)— The reasoner's write-capable dispatch: applies theMemoryWriteToolop to the liveMemoryStore, archives any displaced entry, persistsMEMORY.md, re-renders## MEMORY, and re-prompts a short confirmation. Advisory — a persist failure logs, never raises._dispatch_memory_search(call, *, traceparent)— Read-only:MemoryStore.searchover the archive (superseded/deleted entries; current memory is already in## MEMORY), re-prompted with the hits. No actuation, no file write._emit_scene_objects_span()— Dashboard telemetry: when a spatial-memory backend is wired, publishes theworld.scene_objectsspan for the scene-objects card + SLAM-map overlay. Advisory only; failures swallowed at DEBUG.is_collective_target(text) -> bool— Imported fromopenral_core(single source of truth, shared with theGroundedSubtaskschema validator); true when a task text targets a set. Drives the execute grounding gate._emit_enumeration_invite(task, *, traceparent)— Grounding gate's self-prompt: tells the LLM the active task targets a collective set and todecompose_missioninto one concrete subtask per object before any actuation._dispatch_execute_rskill(call, *, traceparent)— Probes the action server; when it is not on the graph yet,_defer_until_execute_serverholds the busy latch and re-probes for up toexecute_server_wait_sbefore emitting aKIND_CONTROLLERFailureTrigger. Refuses a collective-target task without recording an attempt (grounding gate), and a second goal while one is in flight (busy gate, watchdog-bounded so a dead runner can't wedge the latch forever). A two-tier VRAM/reward-fit check refuses before dispatch rather than risking a slow CUDA OOM abort. Routes GPU-peer eviction through_free_vram_peers_then_sendwhen configured._send_execute_rskill_goal(call, generation, traceparent)— BuildExecuteRskill.Goal, send asynchronously with a feedback callback, attach the generation-bound goal-response handler._free_vram_peers_then_send(call, peers, generation, traceparent)— Deactivate each GPU lifecycle peer and send the goal only once every deactivation response returns, so a peer's VRAM is released before the runner loads the policy. Peers whose service is absent are skipped; a late deactivation from an invalidated generation is immediately reactivated instead of sending a stale goal._reactivate_vram_peers()— Reactivate the peers deactivated for the last dispatch (idempotent). Called on terminal result and on goal reject/error, not on a deadline (the policy may still be resident)._on_reactivate_result(peer, future)— Best-effort log of a reactivationchange_stateoutcome._change_state_async(node, transition) -> future | None— Shared helper: lazily cache aChangeStateclient per peer node and call it asynchronously;Noneif the service isn't on the graph. Used by both lifecycle dispatch and VRAM eviction._dispatch_lifecycle_transition(call)— Drive<call.node>/change_statevia_change_state_async; on success attach_on_lifecycle_response, on an absent service log + skip.- module constant
_FEEDBACK_LOG_PERIOD_S: float = 1.0(L442) — WARNING-log throttle period for per-chunk action feedback. _on_execute_skill_feedback(rskill_id, feedback_msg)— Forward action feedback to the operator log, throttled to one WARNING per_FEEDBACK_LOG_PERIOD_S(a VLA goal streams one feedback per chunk); suppressed lines go to DEBUG._on_execute_skill_goal_response(call, generation, sent_at, future, traceparent)— Ignore/cancel stale generations; on rejection emit aFailureTrigger; on acceptance arm the deadline timer and attach the result handler._on_execute_skill_result(call, generation, goal_id, future, traceparent)— Ignore stale generations, cancel the deadline timer; log success or emit aFailureTriggerwithControllerEvidenceon abort/cancel/failure. The replanning ladder classifies on the typedfailure_kind, not the prose: a config/capability-mismatch result drops that rSkill from the palette until rebuilt; a deadline-miss drives a "timed out" reflection; controller/runtime failures remain retryable.- module constant
_LEGACY_FAILURE_REASON_PREFIXES: tuple[tuple[str, str], ...](L489) — deprecated-in-place prose→FAILURE_*prefix table, consulted only when a failed result carriesFAILURE_NONE(a pre-failure_kindproducer). _rskill_failure_kind(result) -> int— Typed failure kind for a terminal result; the runner-stamped uint8 is authoritative, falling back to the deprecated prose-prefix table only for a producer that predates the field._failure_kind_value(name) -> int— Resolve anExecuteRskill.ResultFAILURE_*constant by name off the generated IDL rather than re-declaring the values, so the reasoner cannot drift frompackages/msgs.- module constant
_PERMANENT_FAILURE_KIND_NAMES: frozenset[str](L509) —{"FAILURE_CONFIG_ERROR", "FAILURE_CAPABILITY_MISMATCH"}. - module constant
_TIMED_OUT_FAILURE_KIND_NAMES: frozenset[str](L515) —{"FAILURE_DEADLINE_MISSED"}. _palette_after_rskill_failure(palette, rskill_id, failure_kind) -> ToolPalette— Drop a skill after a typed, session-persistent availability failure, classified onfailure_kind(never on prose);FAILURE_NONEis "unclassified", never "permanently broken"._on_execute_skill_deadline(*, call, generation, sent_at, goal_handle, traceparent)— Ignore stale generations; latch a"patience"cancel reason (a patience-expired attempt still runs the reward verify gate), cancel the goal, and emit aKIND_TIMEOUTFailureTrigger.- Ladder persistence.
_persist_ladder_state()snapshotsReasonerLadderStateto theladder_state_pathparam after every ledger mutation;_maybe_restore_ladder_state()reloads it aton_configure, so a restart resumes the ladder instead of resetting every cap mid-mission. _on_lifecycle_response(call, future)— Log theChangeStateresult; lifecycle failures surface in the target node's own logs (noFailureTriggerre-emission)._publish_skill_failure(*, kind, rskill_id, evidence, traceparent, trace_id=None)— Build + publish aFailureTriggeron/openral/failure/rskill, then mirror it onto the OTLP span path via_emit_skill_failure_eventso the OTLP-only dashboard can tally it._emit_skill_failure_event(*, kind, rskill_id, evidence)— Stamp anopenral.event.skill_failurespan event carrying the failure state, on the active tick span when recording or a transient span otherwise. Drives the dashboard "skill failures" counter.renderer(self) -> ContextRenderer(L5387) — Direct read access for tests asserting buffer state.dispatched_calls(self) -> tuple[Any, ...](L5392) — Snapshot of tool calls the reasoner has dispatched (in order).set_palette(self, palette) -> None(L5396) — Replace the active palette (rebuilt on/openral/skill_registry_changed)._QOS_REGISTRY_CHANGED(L243) — RELIABLE + TRANSIENT_LOCAL + KEEP_LAST=1 so a late-subscribing reasoner sees the most recent invalidation.main(args=None) -> int(L5401) — Entry point forros2 run openral_reasoner_ros reasoner_node.
packages/openral_prompt_router/openral_prompt_router/prompt_router_node.py
Single lifecycle node that fans in operator prompts from any external source into /openral/prompt. CLI is the only v1 adapter; WebSocket / voice / Slack are out of scope.
- module constant
DEFAULT_SOURCES: dict[str, int] = {"cli": 100, "dashboard": 100, "auto": 10}— Default source → priority registry; human sources get 100, machine cascades get 10. (L76) - module constant
_QOS_PROMPT(L60) —KEEP_LAST=10, RELIABLE, VOLATILE, the fan-out publisher / per-source subscriber QoS. - module constant
_STARTUP_PROMPT_SUBSCRIBER_TIMEOUT_S: float = 30.0(L71) — how longon_activatewaits for a subscriber before publishing the configured startup prompt anyway. class PromptRouterNode(LifecycleNode)(L83) — Lifecycle node.__init__(*, node_name="openral_prompt_router", sources=None)— Initialise with a source → priority registry. Defaults toDEFAULT_SOURCES.on_configure(self, state) -> TransitionCallbackReturn(L119) — Build the/openral/promptfan-out publisher and one/openral/prompt_in/<source>subscriber per allowed source.on_activate(self, state) -> TransitionCallbackReturn(L141) — Publish the startup prompt if configured; then idle (purely reactive).on_deactivate(self, state) -> TransitionCallbackReturn(L150) — Stop forwarding (subscriptions remain attached).on_cleanup(self, state) -> TransitionCallbackReturn(L156) — Drop the publisher; subscriptions auto-cleaned by rclpy._on_inbound(source, priority, msg)— Forward the inbound PromptStamped onto/openral/promptafter merging{"source": ..., "priority": ...}intometadata_json(preserving any per-source fields).forwarded_count(self) -> int(L238) — Number of prompts forwarded sinceon_configure(for tests).main(args=None) -> int(L243) — Entry point forros2 run openral_prompt_router prompt_router_node.
python/cli/src/openral_cli/prompt.py
openral prompt "do X" CLI adapter. Publishes a one-shot PromptStamped onto /openral/prompt_in/cli for the prompt-router to fan out. rclpy lazy-imported so openral --help stays sub-second.
prompt_command(text, topic="/openral/prompt_in/cli", wait_s=1.0, discovery_wait_s=5.0, new_goal=False)(L31) — Initialise rclpy, publish one PromptStamped withmetadata_json={"source_cli": true}plus"new_goal": trueunder--new-goal, wait briefly for the subscriber to be discovered, then shut down. Exits 2 if rclpy / openral_msgs are not importable (with a hint atjust ros2-build). The prompt-router preserves the mission-replacement flag when stampingsource/priority.
Observability (Layer 6 — fully shipped)
python/observability/src/openral_observability/_sdk.py
Idempotent OTel SDK setup + flush helper.
configure_observability(*, service_name="openral", endpoint=None, sample_ratio=None) -> bool— Install OTLP/gRPC tracer + meter + logger providers; readsOTEL_EXPORTER_OTLP_ENDPOINTwhenendpointis None;Trueif exporters were installed,Falsefor the no-op path. On success also starts the system-metrics collector and registersshutdown_observabilityviaatexit.sample_ratio(orOPENRAL_OTEL_SAMPLE_RATIO) selects the trace sampler:None/1.0→ always-on, else a parent-based ratio sampler. (L115)configure_worker_observability(service_name, *, endpoint=None, sample_ratio=None) -> bool— Cross-process bootstrap for a spawned worker: callsconfigure_observabilitythen attaches the parent trace context from env, so a child spawned withenv={**os.environ, **traceparent_env()}joins the parent trace. (L240)_resolve_sampler(sample_ratio) -> Sampler— Resolve the trace sampler from arg + env, defaulting to always-on; a garbage env value also falls back to always-on rather than silently dropping every span. (L353)shutdown_observability() -> None— Flush + shut down all three providers; idempotent and safe with no exporter installed. Stops the system-metrics collector before draining the meter so the final sample lands in the export batch. (L410)
python/observability/src/openral_observability/tracing.py
Span-context-manager helpers; safe to call before configure_observability.
rskill_span(name, *, rskill_id=None, role=None, **attrs)— Span for a Skill lifecycle phase; emitsrskill.id/rskill.rolefromsemconv. (L61)inference_span(name="skill.chunk_inference", *, chunk_index=None, kind: InferenceKind="foreground", **attrs)— Span for one VLA inference plus theopenral.inference.durationhistogram (emitted together so they cannot diverge).InferenceKind = Literal["foreground", "prefetch", "single"]is the closed timing-axis label set. (L91)safety_span(name="safety.check", *, check_name=None, severity="info", **attrs)— Span for a safety check; the C++ kernel parents its ownsafety.checkto the Python tick via the propagator. (L145)reasoner_span(name="reasoner.tick", *, tick_idx=None, model=None, force=None, **attrs)— Span for oneReasonerCore.tick. Setsreasoner.{tick.idx, model, force}and accepts any extrareasoner.*attribute via**attrs. (L179)start_reasoner_span(name="reasoner.tick", *, tick_idx=None, model=None, force=None, **attrs) -> Span— Non-attaching variant for the phased tick: returns a started span without attaching it, soprepare_tick→ off-threadrun_prepared_llm→finish_tickcan carry it between phases (each re-attaching viause_span) without an intermediate executor callback seeing it as current. Caller ownsspan.end(). (L250)- module constant
P(L52) —ParamSpec("P"), generic parameter type var fortraced's decorator signature. - module constant
R(L53) —TypeVar("R"), generic return type var fortraced's decorator signature. traced(name=None)— Decorator that wraps a sync function in a span named after it. (L278)
python/observability/src/openral_observability/cli.py
Root-span helper for the openral CLI.
cli_command_span(subcommand, *, mode=None, run_id=None, **attrs)— Open thecli.commandroot span for one CLI invocation; recordscli.subcommand,openral.run.id, optionalopenral.run.mode/openral.run.git_sha. (L51)
python/observability/src/openral_observability/diagnostics.py
diagnostic_msgs/DiagnosticArray heartbeat helper, shared by every OpenRAL lifecycle node.
Level— Mirror ofdiagnostic_msgs/DiagnosticStatuslevel constants (OK=0,WARN=1,ERROR=2,STALE=3); re-exported sostatus_fncallbacks can avoid importingdiagnostic_msgson pure-Python hosts. (L31)class DiagnosticsHeartbeat(L48) — 1 Hz/diagnosticspublisher attached to arclpy.lifecycle.LifecycleNode.__init__(node, *, hardware_id, component_name, status_fn, rate_hz=1.0). An exception insidestatus_fnis converted to a synthetic ERROR-level diagnostic so the timer never crashes the node.hardware_id(self) -> str(L112) — Return the configuredhardware_idfield.component_name(self) -> str(L117) — Return the configuredDiagnosticStatus.namevalue.create_publisher(self) -> None(L121) — Open the/diagnosticspublisher. Call fromon_configure.start(self) -> None(L138) — Start the 1 Hz publish timer. Call fromon_activate.stop(self) -> None(L155) — Cancel the publish timer. Call fromon_deactivate.destroy(self) -> None(L167) — Destroy the publisher. Call fromon_cleanup/on_shutdown.publish_once(self) -> None(L179) — Publish oneDiagnosticArrayimmediately.
python/observability/src/openral_observability/lifecycle.py
Make LifecycleNode transition-callback failures observable — rclpy silently converts an uncaught callback exception to TransitionCallbackReturn.ERROR without logging, so a composing host sees only exit code 4.
log_lifecycle_errors(callback) -> callback— Decorator foron_configure/on_activate/… transition callbacks. Transparent on success; on an uncaught exception logs the callback name + traceback and returnsTransitionCallbackReturn.FAILUREinstead of letting rclpy swallow it silently. Applied toRskillRunnerNode,_WorldStateLifecycleNode,HALLifecycleNodeBase, andReasonerNode. Lazy-importsrclpyso the module stays import-safe on pure-Python hosts. (L44)
python/observability/src/openral_observability/semconv.py
Single source of truth for OpenRAL OTel attribute / span / metric names. Final[str] constants grouped by wire namespace exactly as the module's own # ── section banners divide them; each constant's value is its own description.
rskill.* — short-prefix span/metric attributes
- const RSKILL_ID: str = "rskill.id" (L30)
- const RSKILL_ROLE: str = "rskill.role" (L31)
- const RSKILL_TICK_MS: str = "rskill.tick_ms" (L33)
- const RSKILL_INFERENCE_MS: str = "rskill.inference_ms" (L34)
- const RSKILL_SENSORS_MS: str = "rskill.sensors_ms" (L35)
- const RSKILL_WORLD_STATE_MS: str = "rskill.world_state_ms" (L36)
- const RSKILL_SAFETY_MS: str = "rskill.safety_ms" (L37)
- const RSKILL_HAL_MS: str = "rskill.hal_ms" (L38)
- const RSKILL_ACTION_APPLIED: str = "rskill.action_applied" (L39)
- const RSKILL_SAFETY_VIOLATIONS: str = "rskill.safety_violations" (L40)
- const RSKILL_EPISODE_IDX: str = "rskill.episode_idx" (L41)
- const RSKILL_STEP_IDX: str = "rskill.step_idx" (L42)
- const RSKILL_REWARD: str = "rskill.reward" (L43)
- const RSKILL_TERMINATED: str = "rskill.terminated" (L44)
- const RSKILL_TRUNCATED: str = "rskill.truncated" (L45)
- const INFERENCE_KIND: str = "inference.kind" (L47)
- const INFERENCE_CHUNK_INDEX: str = "inference.chunk_index" (L48)
- const INFERENCE_CHUNK_SIZE: str = "inference.chunk_size" (L49)
- const INFERENCE_ENGINE: str = "inference.engine" (L50)
- const INFERENCE_DEVICE: str = "inference.device" (L51)
- const INFERENCE_DURATION_MS: str = "inference.duration_ms" (L52)
- const SAFETY_SEVERITY: str = "safety.severity" (L54)
- const SAFETY_CHECK_NAME: str = "safety.check_name" (L55)
- const SAFETY_KERNEL: str = "safety.kernel" (L56)
reward.* — task-progress reward monitor scores
- const REWARD_PROGRESS: str = "reward.progress" (L59)
- const REWARD_SUCCESS: str = "reward.success" (L60)
- const REWARD_STALLED: str = "reward.stalled" (L61)
- const REWARD_SUCCEEDED: str = "reward.succeeded" (L62)
- const REWARD_FRAMES: str = "reward.frames" (L63)
- const REWARD_TASK: str = "reward.task" (L64)
- const REWARD_CAMERA: str = "reward.camera" (L65)
openral.run.* — CLI invocation
- const RUN_ID: str = "openral.run.id" (L69)
- const RUN_MODE: str = "openral.run.mode" (L70)
- const RUN_GIT_SHA: str = "openral.run.git_sha" (L71)
openral.tick.* — runner cadence
- const TICK_IDX: str = "openral.tick.idx" (L75)
- const TICK_RATE_HZ: str = "openral.tick.rate_hz" (L76)
- const TICK_DEADLINE_MS: str = "openral.tick.deadline_ms" (L77)
openral.rskill.* — rSkill identity (namespaced)
- const RSKILL_ID_NS: str = "openral.rskill.id" (L81)
- const RSKILL_REVISION_NS: str = "openral.rskill.revision" (L82)
- const RSKILL_ROLE_NS: str = "openral.rskill.role" (L83)
- const RSKILL_ACTION_HORIZON: str = "openral.rskill.action_horizon" (L84)
openral.hal.*
- const HAL_ADAPTER: str = "openral.hal.adapter" (L88)
- const HAL_ROBOT_MODEL: str = "openral.hal.robot.model" (L89)
- const HAL_CONTROL_MODE: str = "openral.hal.control_mode" (L90)
- const HAL_JOINT_NAMES: str = "openral.hal.joint.names" (L95)
- const HAL_JOINT_POSITIONS: str = "openral.hal.joint.positions" (L96)
- const HAL_JOINT_VELOCITIES: str = "openral.hal.joint.velocities" (L97)
- const HAL_JOINT_EFFORTS: str = "openral.hal.joint.efforts" (L98)
- const HAL_JOINT_POSITION_LIMITS_LO: str = "openral.hal.joint.position_limits_lo" (L99)
- const HAL_JOINT_POSITION_LIMITS_HI: str = "openral.hal.joint.position_limits_hi" (L100)
- const HAL_JOINT_VELOCITY_LIMITS: str = "openral.hal.joint.velocity_limits" (L101)
- const HAL_JOINT_EFFORT_LIMITS: str = "openral.hal.joint.effort_limits" (L102)
- const HAL_JOINT_STAMP_NS: str = "openral.hal.joint.stamp_ns" (L103)
- const HAL_ACTION_NEXT: str = "openral.hal.action.next" (L110)
- const HAL_ACTION_DIM: str = "openral.hal.action.dim" (L111)
- const HAL_ACTION_HORIZON: str = "openral.hal.action.horizon" (L112)
- const HAL_ACTION_APPLIED: str = "openral.hal.action.applied" (L113)
- const HAL_EE_NAMES: str = "openral.hal.ee.names" (L117)
- const HAL_EE_POSE_PREFIX: str = "openral.hal.ee.pose" (L118)
- const HAL_GRIPPER_POSITION: str = "openral.hal.gripper.position" (L119)
- const HAL_GRIPPER_FORCE_N: str = "openral.hal.gripper.force_n" (L120)
openral.sensors.*
- const SENSORS_MODALITY: str = "openral.sensors.modality" (L124)
- const SENSORS_SOURCE: str = "openral.sensors.source" (L125)
- const SENSORS_AGE_MS: str = "openral.sensors.age_ms" (L126)
- const SENSORS_WIDTH: str = "openral.sensors.width" (L127)
- const SENSORS_HEIGHT: str = "openral.sensors.height" (L128)
- const SENSORS_CHANNELS: str = "openral.sensors.channels" (L129)
- const SENSORS_ENCODING: str = "openral.sensors.encoding" (L130)
- const SENSORS_THUMBNAIL_JPEG_B64: str = "openral.sensors.thumbnail_jpeg_b64" (L135)
openral.system.* — host / GPU / CPU health
- const SYSTEM_GPU_INDEX: str = "openral.system.gpu.index" (L139)
- const SYSTEM_GPU_NAME: str = "openral.system.gpu.name" (L140)
openral.world_state.*
- const WORLD_STATE_STALENESS_MS: str = "openral.world_state.staleness_ms" (L144)
- const WORLD_STATE_COMPONENTS_STALE: str = "openral.world_state.components_stale" (L145)
- const WORLD_STATE_HAS_LATCHED_ERROR: str = "openral.world_state.has_latched_error" (L146)
- const WORLD_STATE_COMPONENT: str = "openral.world_state.component" (L147)
openral.world_state.scene_objects.* (durable spatial-memory scene-object graph)
- const WORLD_SCENE_OBJECTS_LIST: str = "openral.world_state.scene_objects.list" (L153)
- const WORLD_SCENE_OBJECTS_COUNT: str = "openral.world_state.scene_objects.count" (L154)
- const WORLD_SCENE_OBJECTS_FRAME: str = "openral.world_state.scene_objects.frame_id" (L155)
- const WORLD_SCENE_OBJECTS_SOURCE_NODE: str = "openral.world_state.scene_objects.source_node" (L156)
openral.dataset.*
- const DATASET_REPO_ID: str = "openral.dataset.repo_id" (L160)
- const DATASET_EPISODE_IDX: str = "openral.dataset.episode_idx" (L161)
- const DATASET_FRAME_IDX: str = "openral.dataset.frame_idx" (L162)
- const DATASET_EPISODE_SUCCESS: str = "openral.dataset.episode.success" (L166)
Span names
- const SPAN_CLI_COMMAND: str = "cli.command" (L170)
- const SPAN_RSKILL_TICK: str = "rskill.tick" (L171)
- const SPAN_RSKILL_CONFIGURE: str = "rskill.configure" (L172)
- const SPAN_RSKILL_ACTIVATE: str = "rskill.activate" (L173)
- const SPAN_RSKILL_EXECUTE: str = "rskill.execute" (L174)
- const SPAN_RSKILL_CHUNK_INFERENCE: str = "rskill.chunk_inference" (L175)
- const SPAN_HAL_READ_STATE: str = "hal.read_state" (L176)
- const SPAN_HAL_SEND_ACTION: str = "hal.send_action" (L177)
- const SPAN_SENSORS_READ_LATEST: str = "sensors.read_latest" (L178)
- const SPAN_WORLD_STATE_SNAPSHOT: str = "world_state.snapshot" (L179)
- const SPAN_WORLD_SCENE_OBJECTS: str = "world.scene_objects" (L180)
- const SPAN_SAFETY_CHECK: str = "safety.check" (L181)
- const SPAN_REWARD_SCORE: str = "reward.score" (L182)
- const SPAN_REASONER_TICK: str = "reasoner.tick" (L184)
- const SPAN_DEPLOY_BRINGUP: str = "deploy.bringup" (L186)
- const BRINGUP_NODE: str = "openral.bringup.node" (L198)
- const BRINGUP_TRANSITION: str = "openral.bringup.transition" (L199)
- const BRINGUP_RESULT: str = "openral.bringup.result" (L200)
- const REASONER_MODEL: str = "reasoner.model" (L203)
- const REASONER_TICK_IDX: str = "reasoner.tick.idx" (L204)
- const REASONER_TOOL: str = "reasoner.tool" (L205)
- const REASONER_RSKILL_ID: str = "reasoner.rskill_id" (L206)
- const REASONER_SUPPRESSED_REASON: str = "reasoner.suppressed_reason" (L207)
- const REASONER_ERROR_KIND: str = "reasoner.error_kind" (L208)
- const REASONER_FORCE: str = "reasoner.force" (L209)
- const REASONER_LLM_S: str = "reasoner.llm_s" (L214)
- const REASONER_PROMPT_TOKENS: str = "reasoner.prompt_tokens" (L216)
- const REASONER_MISSION_JSON: str = "reasoner.mission_json" (L222)
- const SKILL_FAILURE_STATE: str = "openral.event.skill_failure.state" (L227)
- const REASONER_TIER: str = "reasoner.tier" (L232)
- const SPAN_SIM_RUN: str = "sim.run" (L234)
- const SPAN_SIM_STEP: str = "sim.step" (L235)
- const SPAN_PHYSICS_STEP: str = "physics.step" (L236)
Span-event names
- const EVENT_ESTOP_REQUESTED: str = "openral.event.estop_requested" (L254)
- const EVENT_SENSOR_STALE: str = "openral.event.sensor_stale" (L255)
- const EVENT_STALENESS_LATCHED: str = "openral.event.staleness_latched" (L256)
- const EVENT_ERROR_LATCHED: str = "openral.event.error_latched" (L257)
- const EVENT_SAFETY_VIOLATION: str = "openral.event.safety_violation" (L258)
- const EVENT_ACTION_DROPPED: str = "openral.event.action_dropped" (L261)
- const EVENT_DEADLINE_MISSED: str = "openral.event.deadline_missed" (L262)
- const EVENT_SKILL_FAILURE: str = "openral.event.skill_failure" (L269)
- const EVENT_CHUNK_PREFETCH_HIT: str = "openral.event.chunk_prefetch_hit" (L273)
- const EVENT_CHUNK_PREFETCH_MISS: str = "openral.event.chunk_prefetch_miss" (L274)
- const EVENT_EPISODE_CLOSED: str = "openral.event.episode_closed" (L279)
Metric instrument names
- const METRIC_TICK_DURATION: str = "openral.tick.duration" (L283)
- const METRIC_INFERENCE_DURATION: str = "openral.inference.duration" (L284)
- const METRIC_TICK_BUDGET_VIOLATIONS: str = "openral.tick.budget_violations" (L285)
- const METRIC_TICK_DEADLINE_MISSES: str = "openral.tick.deadline_misses" (L286)
- const METRIC_SAFETY_VIOLATIONS: str = "openral.safety.violations" (L287)
- const METRIC_HAL_READ_STATE_DURATION: str = "openral.hal.read_state.duration" (L288)
- const METRIC_HAL_SEND_ACTION_DURATION: str = "openral.hal.send_action.duration" (L289)
- const METRIC_HAL_ESTOP_COUNT: str = "openral.hal.estop.count" (L290)
- const METRIC_SENSORS_AGE_MS: str = "openral.sensors.age_ms" (L291)
- const METRIC_SENSORS_STALE_READS: str = "openral.sensors.stale_reads" (L292)
- const METRIC_WORLD_STATE_STALENESS_MS: str = "openral.world_state.staleness_ms" (L293)
- const METRIC_WORLD_STATE_COMPONENTS_STALE: str = "openral.world_state.components_stale" (L294)
- const METRIC_OBSERVABILITY_EXPORT_FAILURES: str = "openral.observability.export_failures" (L295)
- const METRIC_SIM_EPISODE_COUNT: str = "openral.sim.episode.count" (L296)
- const METRIC_SYSTEM_GPU_MEMORY_USED_MB: str = "openral.system.gpu.memory_used_mb" (L300)
- const METRIC_SYSTEM_GPU_MEMORY_TOTAL_MB: str = "openral.system.gpu.memory_total_mb" (L301)
- const METRIC_SYSTEM_GPU_UTIL_PCT: str = "openral.system.gpu.utilization_pct" (L302)
- const METRIC_SYSTEM_CPU_UTIL_PCT: str = "openral.system.cpu.utilization_pct" (L303)
- const METRIC_SYSTEM_RAM_USED_MB: str = "openral.system.ram.used_mb" (L304)
- const METRIC_SYSTEM_RAM_TOTAL_MB: str = "openral.system.ram.total_mb" (L305)
- const METRIC_SIM_EPISODE_SUCCESS: str = "openral.sim.episode.success" (L306)
Closed-set label vocabularies (for metric labels)
- const LABEL_RSKILL_ID: str = "rskill.id" (L313)
- const LABEL_RSKILL_REVISION: str = "rskill.revision" (L314)
- const LABEL_HAL_ADAPTER: str = "hal.adapter" (L315)
- const LABEL_ROBOT_MODEL: str = "robot.model" (L316)
- const LABEL_CONTROL_MODE: str = "control_mode" (L317)
- const LABEL_ENGINE: str = "engine" (L318)
- const LABEL_DEVICE: str = "device" (L319)
- const LABEL_KIND: str = "kind" (L320)
- const LABEL_MODALITY: str = "modality" (L321)
- const LABEL_CHECK_NAME: str = "check_name" (L322)
- const LABEL_SEVERITY: str = "severity" (L323)
- const LABEL_SIGNAL_KIND: str = "signal_kind" (L324)
- const LABEL_COMPONENT: str = "component" (L325)
- const LABEL_REASON: str = "reason" (L326)
- const METRIC_THRESHOLD_MS: str = "openral.metric.threshold_ms" (L332)
- const METRIC_THRESHOLD_DIR: str = "openral.metric.threshold_dir" (L338)
- const THRESHOLD_DIR_UPPER: str = "upper" (L339)
- const THRESHOLD_DIR_LOWER: str = "lower" (L340)
Run-mode enum (closed set for openral.run.mode)
- const RUN_MODE_SIM: str = "sim" (L344)
- const RUN_MODE_HARDWARE: str = "hardware" (L345)
- const RUN_MODE_BENCHMARK: str = "benchmark" (L346)
Safety kernel enum (closed set for safety.kernel — see SAFETY_KERNEL)
- const SAFETY_KERNEL_NULL: str = "null" (L350)
- const SAFETY_KERNEL_CPP: str = "cpp" (L351)
python/observability/src/openral_observability/metrics.py
Cached OTel meter instruments — safe to call before configure_observability.
get_meter() -> Meter— Resolve the OpenRAL meter against the currentMeterProvider. (L64)get_tick_duration() -> Histogram—openral.tick.duration, unitms. (L99)get_inference_duration() -> Histogram—openral.inference.duration, unitms. (L114)get_hal_read_state_duration() -> Histogram—openral.hal.read_state.duration, unitms. (L126)get_hal_send_action_duration() -> Histogram—openral.hal.send_action.duration, unitms. (L138)get_sensors_age_ms() -> Histogram—openral.sensors.age_ms, unitms. (L150)get_world_state_staleness_ms() -> Histogram—openral.world_state.staleness_ms, unitms. (L162)get_tick_budget_violations() -> Counter—openral.tick.budget_violations. (L177)get_tick_deadline_misses() -> Counter—openral.tick.deadline_misses. (L188)get_safety_violations() -> Counter—openral.safety.violations, labelscheck_name/severity. (L199)get_hal_estop_count() -> Counter—openral.hal.estop.count. (L213)get_sensors_stale_reads() -> Counter—openral.sensors.stale_reads. (L234)get_sim_episode_count() -> Counter—openral.sim.episode.count; sim episodes that ran to completion (terminated or truncated). (L245)get_sim_episode_success() -> Counter—openral.sim.episode.success; sim episodes that hittask.success_keyat least once. (L256)get_observability_export_failures() -> Counter—openral.observability.export_failures, labelsignal_kind. (L267)get_world_state_components_stale() -> UpDownCounter—openral.world_state.components_stale. (L292)get_system_gpu_memory_used_mb() -> UpDownCounter—openral.system.gpu.memory_used_mb, unitMBy. (L306)get_system_gpu_memory_total_mb() -> UpDownCounter—openral.system.gpu.memory_total_mb, unitMBy. (L318)get_system_gpu_util_pct() -> UpDownCounter—openral.system.gpu.utilization_pct, unit%. (L330)get_system_cpu_util_pct() -> UpDownCounter—openral.system.cpu.utilization_pct, unit%. (L342)get_system_ram_used_mb() -> UpDownCounter—openral.system.ram.used_mb, unitMBy. (L354)get_system_ram_total_mb() -> UpDownCounter—openral.system.ram.total_mb, unitMBy. (L366)record_histogram_ms(instrument, value_ms, attributes=None) -> None— Record a millisecond value, skipping negatives andNaN. (L381)
python/observability/src/openral_observability/producer.py
Producer-side helpers for recording rich span attributes on OpenRAL hot-path spans. Safe to call on no-op spans; lists are truncated to _MAX_JOINTS / _MAX_EE_FRAMES and floats rounded to 3 decimals.
record_joint_state(span, *, names, positions, velocities=None, efforts=None, position_limits=None, velocity_limits=None, effort_limits=None, stamp_ns=None) -> None— Attach per-joint attributes to ahal.read_statespan. (L96)record_action(span, *, next_row, dim=None, horizon=None, applied=None, gripper_position=None, gripper_force_n=None) -> None— Attach commanded-action attributes to ahal.send_actionspan. (L144)record_ee_poses(span, ee_poses) -> None— Flatten aname → Pose6Dmapping onto aworld_state.snapshotspan. (L175)record_sensor_frame_attrs(span, *, modality=None, encoding=None, width=None, height=None, channels=None, age_ms=None, thumbnail_bytes=None, thumbnail_already_encoded_b64=False) -> None— Attach sensor-frame attributes to asensors.read_latestspan. (L201)emit_sensor_frame_span(frame, *, sensor_name, age_ms, flip_180=False, tracer_name=…) -> None— The shared producer of a dashboardsensors.read_latestspan for one camera frame: optional flip of a display-only copy (the policy's actual frame is never mutated), then span + attrs + thumbnail. Both frame emitters route through this one function so they can't drift apart. (L245)encode_frame_thumbnail— but the span is opened first and the function returns as soon asspan.is_recording()is false (no OTLP endpoint / dashboard off): the flip copy and the JPEG only feed the span, and at 30 Hz per camera they held ~25 % of the deploy runtime's GIL on an AGX Orin for nothing. The flip is passed toencode_frame_thumbnail; the span never copies a full-resolution flipped frame.encode_rgb_thumbnail(rgb) -> bytes | None— Encode an HWC uint8 RGB ndarray to a small JPEG for OTLP; returnsNoneif Pillow is unavailable. (L300)encode_frame_thumbnail(frame, *, flip_180=False) -> bytes | None— Encode anopenral_core.SensorFrame(RGB8/BGR8/MONO8/JPEG/PNG) as a small JPEG thumbnail; returnsNonefor non-renderable encodings. (L333) Raw frames are subsampled to the 320x240 box before any copy, and the optional 180° flip and BGR swap run on that small view: 0.6 ms per 1920x1200 frame on a Jetson AGX Thor instead of 8.2 ms, which at three cameras x 30 Hz had left a competing Python thread 34 % of its speed. JPEG input decodes in reduced-scale draft mode. Display-only (the dashboard tile); Foxglove reads the full-resolution ROS image topics.modality_for_encoding(encoding) -> str— Map aFrameEncodingto the dashboard's modality label (rgb/mono/depth/raw/unknown); shared byDeployRunnerand the world-state node so both produce identical labels. (L57)_MODALITY_BY_ENCODING: dict[str, str](L45) — Canonical encoding → modality lookup table.
python/observability/src/openral_observability/rclpy_spin.py
Quiet teardown for rclpy executor spin threads.
spin_executor_until_shutdown(executor) -> None(L20) —executor.spin()until a signal-driven shutdown, which ends it quietly:ExternalShutdownException,KeyboardInterruptand aRuntimeError(coversRCLError/InvalidHandle, and a subscription take racing the shutdown) raised after the context went down (the SIGINT handler invalidates it mid-spin) return normally instead of printing a thread traceback; aRuntimeErrorwith the context still up propagates. Thethreading.Threadtarget of the dashboard's e-stop / safety-status / perception-overlay executors and ofRos2ImageSensorReader's private node (the Isaacdeploy simteardown traceback inruntime_node). Third twin ofopenral_hal.lifecycle.spin_until_shutdownandopenral_rskill_ros.compose.spin_until_shutdown; kept in the lowest layer so the runner and dashboard share one copy.spin_node_until_shutdown(node) -> None(L45) —rclpy.spin(node)with the same quiet-teardown contract: spins the node on a privateSingleThreadedExecutorthroughspin_executor_until_shutdown; the caller still destroys the node and callsrclpy.try_shutdown(). Themain()spin ofsegmenter_node(the Isaacdeploy simteardown crash, a take racing the shutdown),reward_monitor_node,scene_vlm_node,depth_provider_nodeandcritic_producer_node.
python/observability/src/openral_observability/system_metrics.py
Background sampler for the openral.system.* gauges; feeds the dashboard's System Health card via psutil (CPU + RAM) and optional pynvml (GPU memory + util).
start_system_metrics_collector(*, interval_s=1.0) -> bool— Start a daemon thread that samples host metrics everyinterval_sseconds. ReturnsFalseand a quiet no-op when neitherpsutilnorpynvmlis importable. Idempotent; re-starts retune the interval. (L46)stop_system_metrics_collector(*, timeout_s=2.0) -> None— Signal the collector thread to stop and join. Safe to call when not running. (L76)_nvml_query(read, what, gpu_index) -> Any | None— Run one NVML read, returningNonewhen the device does not implement it. Each GPU metric is queried independently, so an unsupported call (e.g. no discrete VRAM pool on a unified-memory SoC) cannot also drop the other metrics. Logged once per (device, query). (L146)
python/observability/src/openral_observability/propagation.py
W3C TraceContext inject / extract for cross-process trace correlation.
current_traceparent() -> str | None— W3Ctraceparentvalue for the active span, orNoneoutside a span. (L54)current_trace_id() -> str | None— The 32-hex trace id of the active span, orNoneoutside a span (including no-op observability mode). The id half ofcurrent_traceparent, for the fields that store a bare pointer into the trace tree —RunResult.trace_id,RSkillEvalResult.trace_id. (L69)inject_traceparent(carrier=None) -> dict[str, str]— Write the active span'straceparent(and optionaltracestate) into a carrier dict; used by producers ofActionChunk.msg/ExecuteRskill.action/FailureTrigger.msg. (L87)extract_traceparent(traceparent, tracestate=None) -> Context— Parse a wire-sidetraceparentinto an OTelContextforcontext.attach/trace.use_span; consumed by the C++ safety kernel and any Python ROS consumer. (L117)traceparent_env(carrier=None) -> dict[str, str]— Env-var carrier (OTEL_TRACEPARENT+ optionalOTEL_TRACESTATE) for the active span, built frominject_traceparent; pass asenv=tosubprocess/multiprocessingso a worker joins the parent trace.{}when no valid span is in scope. (L153)attach_traceparent_from_env(env=None) -> object | None— Worker-side counterpart: readOTEL_TRACEPARENT/OTEL_TRACESTATEfromenv(defaultos.environ) andcontext.attachthe parent context; returns the detach token, orNonewhen absent/empty. (L195)remote_parent_from_env(env=None)[@contextmanager] — Scopeattach_traceparent_from_envfor a workermain(): attaches on enter, detaches on exit; yields the detach token (orNonewhen no carrier present). (L238)
python/observability/src/openral_observability/failure_bus.py
Publisher helper + IDL-mirror constants for the namespaced /openral/failure/{...} bus.
class FailureSource(str, Enum)(L113) —HAL | SENSOR | SKILL | SAFETY | WAM | CRITIC; the string value is the topic suffix.topic_for(source: FailureSource) -> str(L128) — Pure helper:FailureSource → /openral/failure/<suffix>.KIND_*/SEVERITY_*intmodule constants (L105–L121) — Mirroropenral_msgs/msg/FailureTrigger(incl.KIND_COLLISION = 10, stamped by the safety kernel on every collision stop); bump both when the IDL changes. A test pins the two sides name-for-name and value-for-value against the generated constants.KIND_TIMEOUT: int = 0(L92)KIND_FORCE: int = 1(L93)KIND_WORKSPACE: int = 2(L94)KIND_PERCEPTION: int = 3(L95)KIND_CRITIC: int = 4(L96)KIND_CONTROLLER: int = 5(L97)KIND_SELFVERIFY: int = 6(L98)KIND_HUMAN: int = 7(L99)KIND_WAM: int = 8(L100)KIND_REASONER_TIMEOUT: int = 9(L101)KIND_COLLISION: int = 10(L102)KIND_SUPPRESSED_SUMMARY: int = 254(L103)SEVERITY_INFO: int = 0(L105)SEVERITY_WARN: int = 1(L106)SEVERITY_FAIL: int = 2(L107)SEVERITY_ABORT: int = 3(L108)TOPIC_PREFIX: str = "/openral/failure"(L110)DEFAULT_RATE_LIMIT_HZ: dict[int, float | None](L146) — Per-severity defaults (INFO/WARN → 10/s, FAIL/ABORT → unlimited).DEFAULT_SUMMARY_PERIOD_S: float = 1.0(L153)class _TokenBucket(L156) — Private, lock-protected.__init__(rate_hz, *, capacity=1.0, clock=time.monotonic);try_consume() -> bool.class FailureBusPublisher(L205) —__init__(node, source, *, rate_limit_hz=None, summary_period_s=1.0, clock=None). Configure the publisher; does not open the ROS publisher.topic(self) -> str(L268) — The full topic this publisher writes to.source(self) -> FailureSource(L273) — The source layer this publisher represents.create_publisher(self) -> None(L277) — Open the ROS 2 publisher with the bus QoS profile.start(self) -> None(L299) — Start the 1 HzKIND_SUPPRESSED_SUMMARYroll-up timer.stop(self) -> None(L312) — Cancel the suppressed-summary timer (on_deactivate).destroy(self) -> None(L318) — Tear down the publisher + timer (on_cleanup).publish(self, *, kind, severity, evidence, rskill_id='', trace_id=None) -> bool(L329) — Emit oneFailureTrigger, subject to rate-limiting (Falsewhen rate-limited).
python/observability/src/openral_observability/logging.py
trace_context_processor(_logger, _method_name, event_dict)— structlog processor that stampstrace_id/span_idon every log event. (L59)resolve_log_level() -> int(L75) — Resolve the OpenRAL log floor fromOPENRAL_LOG_LEVEL(name or int); defaults toINFO, notDEBUG; an unparseable value falls back to the default rather than raising. Below the floor the stdlib check short-circuits before rendering/shipping, which matters since several DEBUG call sites fire per control tick. Governs log records only — dashboard span rows are banded separately bydashboard.store._is_headline_span.apply_structlog_level_floor()— Configure structlog's stock renderer to drop events belowresolve_log_level(); called byconfigure_observabilityon its no-endpoint path, where the stock config otherwise printed every DEBUG event (a deploy runtime emits one per 750 Hz joint state and per camera frame — ~10 % of the process's GIL on an AGX Orin). (L103)install_structlog_bridge(logger_provider)— Wire the structlog processor chain to forward records to the OTelLoggerProvider, with both the bridge logger and theopenralroot logger set toresolve_log_level(). (L129)
python/observability/src/openral_observability/dashboard/store.py
In-memory aggregator for openral dashboard — feeds the SSE stream and the /api/state JSON endpoint. Thread-safe, bounded (200 events, 600 metric samples per series). Registered headline span families each populate one card slot in self._topics.
Event-log severity band (distinct from the headline-card routing above). _is_headline_span(name) bands a span's Event Log row: ERROR status → error; a small allow-list of names/prefixes (cli.command, rskill.execute/configure/activate, reasoner.tick, world.scene_objects, sim.run, detect.probe.*) → info; everything else → debug. Deliberately an allow-list rather than a deny-list, so a new high-rate span stays quiet in the event log until promoted here instead of flooding it by default; every span is still indexed in full for openral replay.
class TelemetryEvent— Frozen dataclass holding one event log row (ts_unix,kind,title,attrs,severity). (L229)to_json(self) -> dict[str, Any](L255) — Return a plain-dict view suitable for JSON serialization.class TelemetryStore— Read-side aggregator over OTLP signals. (L381)ingest_spans(payload: list[ResourceSpans]) -> int— Decode + record spans; populates headline cards, increments span-event counters, publishes a delta to every subscriber queue. Returns the number of spans recorded. (L474)ingest_metrics(payload: list[ResourceMetrics]) -> int— Decode + record metric data points; appends per-series samples and tracks cumulative sums. (L508)ingest_logs(self, payload) -> int(L529) — Decode + record OTLPResourceLogs(the structlog→OTel bridge) as event-log rows: body → title, logger name → kind, severity number → level. Shares the bounded event ring with spans; the UI defaults the Debug chip off. Returns the number of log records recorded.snapshot() -> dict[str, Any]— One-shot view: service identity, headline cards, event ring, counters, metric series with p50/p95. (L568)subscribe() -> asyncio.Queue— Register an SSE subscriber. The queue is bounded; on overflow the oldest payload is dropped so the producer never blocks. (L746)unsubscribe(queue) -> None— Drop a subscriber's queue. (L762)set_estopped(self, value) -> None(L573) — Force the e-stop latch flag from an authoritative operator action.set_safety_status(*, latched, drop_reason, drop_reason_label, detail, rskill_id, trace_id, stamp_unix) -> None— Record oneSafetyStatusfrom the latched/openral/safety_statustopic and push it to SSE subscribers. The only authoritative safety state the dashboard has, distinct from the span-inferred latch which stops updating the moment a kernel actually latches.stamp_unixis load-bearing: it lets the card tell a live latch from a dead publisher's leftover durable value. (L591)list_traces(self) -> list[dict[str, Any]](L935) — Return one record per indexed trace_id, most-recent first.-
lookup_trace(self, trace_id) -> list[dict[str, Any]] | None(L953) — Return every indexed span fortrace_idin chronological order, orNone. -
TelemetryStore.set_perception_detections(self, *, camera, detections, model_id, frame_width, frame_height, stamp_unix, flip_180=False) -> None(L644) — Record one detector result as a camera overlay and push it to SSE subscribers.camerais the wiresensor_id, the join key between a frame and its boxes;frame_width/frame_heightlet the tile scale boxes onto its aspect-preserving thumbnail. An emptydetectionslist is recorded, not dropped, so "found nothing" is expressible rather than leaving a stale overlay painted. TelemetryStore.set_perception_masks(self, *, camera, masks, rskill_id, stamp_unix, flip_180=False) -> None(L701) — Record one segmenter result as a camera overlay: plural full-frame masks in the producer's area-ascending order, with advisory scores carried for display only (never used to rank or filter). Fed from the segmenter node's diagnostic re-publication of its one-shot service reply, off by default. Display only — nothing in the safety or attachment path reads this topic.
python/observability/src/openral_observability/dashboard/perception_overlay_subscriber.py
Detector boxes + segmenter masks drawn over the camera tiles. Same shape as safety_status_subscriber.py — one node created at launch, spun on a daemon thread, inert-but-harmless without rclpy / the openral_msgs overlay. Read-only and advisory: it decides what an operator sees, never what the robot does.
- module constant
PERCEPTION_OBJECTS_TOPIC: str = "/openral/perception/objects"— the detector node'soutput_topicdefault. (L50) - module constant
PERCEPTION_MASKS_TOPIC: str = "/openral/perception/masks"(L51) - module constant
PERCEPTION_MASKS_TOPIC: str = "/openral/perception/masks"— the segmenter node'sdebug_masks_topicdefault; a diagnostic re-publication of its latest reply, off by default there, so subscribing costs nothing when nobody enabled it. dashboard_flip_180() -> bool— Whether this host rotates the dashboard's display copy of camera frames, parsingOPENRAL_DASHBOARD_FLIP_180with the same truthy set as the sensor leg and the world-state node. Stamped onto each overlay so the renderer applies exactly the flip the image got. (L66)mono8_mask_to_png_b64(data: bytes, width: int, height: int) -> str— Encode one mono8 mask as a base64 LA PNG whose alpha channel is the mask, so the frontend tints it with one composite instead of decoding pixels in JS. RaisesValueErrorwhendatais not exactlywidth * heightbytes. (L95)class PerceptionOverlaySubscriber(L143) — Opens the node + subscription immediately, at the detector's sensor-class QoS (BEST_EFFORT + VOLATILE + KEEP_LAST=5) — a RELIABLE subscriber would never match and the overlay would sit silently blank.available(self) -> bool(L221) — True when the subscription is live (rclpy +openral_msgspresent).masks_available(self) -> bool(L226) — True when the mask subscription is live (needsSegmentMasksbuilt).close(self) -> None(L313) — Tear down the node/executor and shut rclpy down only if it started it._on_objectsand_on_maskseach drop a malformed payload with a logged decode-failure rather than letting an exception kill the spin thread and end overlays for the session. The mask leg is optional (its ownImportErrorguard) so an olderopenral_msgsbuild withoutSegmentMaskskeeps drawing boxes;masks_availablereports it separately fromavailable. Wired inrun_dashboardontoapp.state.perception_overlay.
python/observability/src/openral_observability/dashboard/estop_publisher.py
Persistent ROS 2 e-stop publisher for the dashboard (safety-critical) — the dashboard's first rclpy publisher. One publisher created at dashboard startup so DDS discovery happens once; a later press publishes instantly instead of racing a fresh subprocess's discovery.
class EstopPublisher— Creates the node +/openral/estop+/openral/estop_clearedpublishers immediately at RELIABLE/VOLATILE/depth-10 QoS, matching the HAL/kernel/runner subscriptions; degrades to inert (callers fall back to the shell-out path) when rclpy/ROS is unavailable. (L26)available(self) -> bool(L81) — True when the persistent publisher is live (rclpy + ROS present).trigger(self) -> bool(L85) — Publish/openral/estopinstantly. ReturnsFalseif unavailable.clear(self) -> bool(L89) — Publish/openral/estop_clearedinstantly. ReturnsFalseif unavailable.close(self) -> None(L103) — Tear down the node + executor; shut down rclpy only if we started it.
python/observability/src/openral_observability/dashboard/safety_status_subscriber.py
The dashboard's first rclpy subscriber: one node created at launch, spun on a daemon thread, inert-but-harmless without rclpy / the openral_msgs overlay. Read-only: no publisher, no service client, no authority over the robot.
- module constant
SAFETY_STATUS_TOPIC: str = "/openral/safety_status"(L32) drop_reason_label(code: int, status_cls: Any) -> str— Name adrop_reasonby reverse-mapping it against the generated message class's own constants, so a constant added toSafetyStatus.msgneeds no second table here; unknown values degrade to"drop_reason_<code>". (L35)class SafetyStatusSubscriber— Opens the node + subscription immediately at the publishers' QoS (RELIABLE + TRANSIENT_LOCAL + KEEP_LAST=1) — a VOLATILE subscriber would never match and the card would sit silently empty. Wired inrun_dashboardontoapp.state.safety_status. (L68)available(self) -> bool(L122) — True when the subscription is live (rclpy +openral_msgspresent).close(self) -> None(L139) — Tear down the node/executor and shuts rclpy down only if it started it.
python/observability/src/openral_observability/dashboard/discovery.py
mDNS advertise + browse for the live dashboard. Optional, requires the mdns extra; when zeroconf is not importable, Discovery stays disabled and the dashboard runs exactly as before.
- module constant
SERVICE_TYPE: str = "_openral-otlp._tcp.local."— mDNS service type for all OpenRAL dashboard OTLP receivers. Single source of truth for the advertiser and browser. (L30) class DiscoveredRobot(BaseModel)— One mDNS-discovered OpenRAL service; the/api/robotswire shape used by external operator tooling. Fields:name: str,addresses: list[str],port: int,properties: dict[str, str] = {},last_seen: float. (L37)class RobotRegistry— Thread-safe map of discovered robots (zeroconf callbacks run off-thread). (L47)upsert(self, robot) -> None(L55) — Insert or replace the entry keyed byrobot.name.remove(self, name) -> None(L64) — Remove the entry for name if it exists (idempotent).list_robots(self) -> list[DiscoveredRobot](L73) — Return all known robots sorted by name.class Discovery— Owns theZeroconfinstance, advertiser, and browser for the dashboard. Attributeenabled: bool. Wired intorun_dashboardinserver.py;app.state.discoveryholds the instance (orNonewhen themdnsextra is absent or zeroconf failed). (L83)robots(self) -> list[DiscoveredRobot](L99) — Delegate to the underlying registry.start(self, *, host, port) -> None(L107) — Start browsing always; advertise only on a non-loopback, non-wildcard bind.stop(self) -> None(L151) — Unregister the advertised service, cancel the browser, and close Zeroconf.
python/observability/src/openral_observability/dashboard/app.py
create_app(store: TelemetryStore | None = None) -> FastAPI— Build the dashboard ASGI app: the SSE/state routes, the OTLP/HTTP receivers, operator write endpoints (/api/prompt,/api/estop_reset), a live MJPEG camera stream, mDNS robot discovery, and the guarded write-controls (/api/skill/execute,/api/param/set, both OFF by default). (L775)
python/observability/src/openral_observability/dashboard/vad_assets.py
class PinnedAsset(NamedTuple)(L52) —url: str,sha256: str,size: intfor one pinned binary asset.- module constant
PINNED_VAD_ASSETS: dict[str, PinnedAsset](L65) — The three voice-prompt binaries no longer committed to git, pinned to exact upstream URLs + sha256 recorded instatic/vendor/vad/NOTICE.md. sha256_of(path: Path) -> str— Hex sha256 digest of the file atpath, read in chunks. (L94)ensure_vad_assets() -> bool(L142) — Best-effort: for each pinned asset, reuse a sha256-verified cache hit or download + verify one, then serve it fromstatic/vendor/vad/. Never raises; a failed asset logs a warning and does not stop the others. Called best-effort fromrun_dashboardon every start (never gates startup).vad_assets_available() -> bool(L208) — Cheap presence-only check (no re-hash) of whether every pinned asset is currently served; backs/api/config'svoice_prompt_enabled.
python/observability/src/openral_observability/dashboard/server.py
run_dashboard(*, host="127.0.0.1", port=4318, inprocess_cmd=None, store=None, log_level="warning") -> None— Start uvicorn onhost:portand block until SIGINT/SIGTERM. Callsvad_assets.ensure_vad_assets()best-effort before binding. Prints a single URL banner to stderr before binding. Wheninprocess_cmdis set, spawns the argv as a child process pointed at this dashboard's OTLP endpoint. Default port4318(OTLP/HTTP standard) rather than8000, to avoid clashing with common dev servers. (L56)spawn_dashboard/attached_dashboard— Inverse of--inprocess; seedashboard/attach.pybelow (a separate module despite the name symmetry with this file).
python/observability/src/openral_observability/dashboard/attach.py
The --inprocess inverse: spawn openral dashboard as a child process and attach the current process's telemetry to it. Used by openral sim run --dashboard / openral deploy run --dashboard / openral benchmark run --dashboard.
- module constant
_LOG(L30) —logging.getLogger(__name__). - module constant
_ENV_ENDPOINT: str = "OTEL_EXPORTER_OTLP_ENDPOINT"(L32) - module constant
_ENV_PROTOCOL: str = "OTEL_EXPORTER_OTLP_PROTOCOL"(L33) - module constant
_HTTP_OK: int = 200(L34) — healthz poll success code. spawn_dashboard(*, host="127.0.0.1", port=4318, ready_timeout_s=10.0) -> Iterator[str | None][@contextmanager] (L38) — Spawnopenral dashboardas a child, set the OTLP env, poll/healthzuntil ready, yield the URL, and SIGINT the child on exit. YieldsNoneifopenralisn't on PATH, the child dies early, or the health check never passes within the timeout._wait_healthy(healthz_url, child, *, timeout_s) -> bool(L202) — Pollhealthz_urluntil it returns_HTTP_OKorchildexits ortimeout_selapses._shutdown_child(child) -> None(L217) — SIGINT the child and wait, escalating if needed.attached_dashboard(*, enabled, port=4318, subcommand=None, mode=None) -> Iterator[bool][@contextmanager] (L129) — Convenience wrapper for CLI commands that gate dashboard attach on a flag.enabled=FalseyieldsFalseas a true no-op (no FastAPI/uvicorn imports).enabled=Truedelegates tospawn_dashboard, re-runsconfigure_observabilityon the new endpoint, and re-opens thecli.commandroot span so workload spans have a real recording ancestor instead of being orphaned under the pre-dashboard root. Drains viashutdown_observabilityinfinally. YieldsTrueiff the child reported healthy.
python/observability/src/openral_observability/dashboard/store.py — F7 trace index additions
Bounded per-trace_id span index for query-time bag↔OTel join.
class _IndexedSpan(L267) — Frozen-ish record retained bytrace_id:name,trace_id,span_id,parent_span_id,start_ns,end_ns,attrs,status_code,status_message,events..to_json()returns a plain dict carryingduration_ms.TelemetryStore.list_traces() -> list[dict]— One row per indexed trace_id (trace_id,span_count,last_seen_unix), most-recent first. BacksGET /api/traces.TelemetryStore.lookup_trace(trace_id: str) -> list[dict] | None— Every indexed span fortrace_id, sorted ascending bystart_unix_ns.Nonewhen the trace is not (or no longer) in the bounded index. BacksGET /api/spans/{trace_id}._TRACE_INDEX_MAX_TRACES = 64/_TRACE_INDEX_MAX_SPANS = 2048— Memory caps. Older trace_ids evict FIFO on insertion.
python/observability/src/openral_observability/dashboard/app.py — F7 routes
GET /api/traces— JSON{"traces": [...]}fromTelemetryStore.list_traces.GET /api/spans/{trace_id}— JSON{"trace_id", "spans": [...]}fromTelemetryStore.lookup_trace; 404 when the trace is not indexed.GET /api/config— JSON{"jaeger_ui_url": "..."}from theOPENRAL_JAEGER_UI_URLenv (default""). The UI uses this to enable/disable the footer "open in jaeger" link rather than guessing alocalhost:16686link that may be dead.
python/observability/src/openral_observability/tracing_lttng.py
Opt-in LTTng tracepoints around the realtime hot path. No-op when OPENRAL_ROS2_TRACING is unset; falls back to JSONL when lttngust is missing.
ENV_TRACING_GATE = "OPENRAL_ROS2_TRACING"(L60) — Truthy values (1/true/yes/on) enable the backend; anything else leaves every tracepoint a no-op.ENV_TRACING_FALLBACK_DIR = "OPENRAL_ROS2_TRACING_FALLBACK_DIR"(L66) — Override for the JSONL fallback directory (default/tmp/openral-lttng-fallback).TP_RUNNER_TICK = "openral:runner_tick"(L74)TP_HAL_READ_STATE = "openral:hal_read_state"(L75)TP_HAL_SEND_ACTION = "openral:hal_send_action"(L76)TP_SENSORS_READ_LATEST = "openral:sensors_read_latest"(L77)TP_WORLD_STATE_SNAPSHOT = "openral:world_state_snapshot"(L78)TP_SKILL_STEP = "openral:skill_step"(L79)TP_ACTION_PUBLISH = "openral:action_publish"(L80)TP_SAFETY_VALIDATE = "openral:safety_validate"(L81) — Tracepoint base names;lttng_tracepointappends_begin/_endsuffixes.is_enabled() -> bool(L112) — Single source of truth for the gate.lttng_tracepoint(name, **attrs) -> Iterator[None](L175) — Context manager that fires<name>_begin/<name>_endaround the block. Attaches the active OTeltrace_idasotel_trace_idso CTF traces can join back to OTel.class LttngSession(name, output_dir)(L93) — Identity of an active session.class LttngSessionError(RuntimeError)(L88) — Raised by the subprocess wrappers.- module constant
_BACKEND_RESOLVED: bool = False(L107) — whether_resolve_backendhas run yet (cached-resolution guard). - module constant
_BACKEND: Any = None(L108) — the cached resolved tracer backend (_LttngUstBackend/_JsonFallbackBackend/None). - module constant
_WARNED_ONCE: bool = False(L109) — one-time fallback-warning latch consumed by_warn_fallback. - module constant
_TRACEPARENT_PART_COUNT: int = 4(L85) — expectedtraceparentdash-separated part count. - module constant
_LOG(L55) —logging.getLogger(__name__). start_session(*, name, output_dir) -> LttngSession(L321) —lttng create / enable-event openral:* / add-context / start.stop_session(*, name) -> None(L346) —lttng stop+destroy(flush + teardown).view_session(*, output_dir) -> None(L358) —babeltrace2 OUTPUT_DIR; falls back to listing files whenbabeltrace2is absent.
python/dataset/src/openral_dataset/recorder.py
In-memory per-rollout accumulator with multi-sink fan-out.
@dataclass class EpisodeHeader(episode_idx, task_string, fps, robot_name, stamp_ns)— Per-episode metadata pushed to sinks atepisode_start. (L55)@dataclass class DatasetFrame(episode_idx, frame_idx, observation_state, images, action, reward, terminated, truncated, stamp_ns, trace_id="", span_id="")— Per-tick frame pushed to sinks atrecord_frame.trace_id(32 hex) /span_id(16 hex) carry the producingrskill.tickspan's ids (ISSUE-109 forward link);""when no valid span was in scope. (L78)@dataclass class EpisodeSummary(episode_idx, success, n_frames, stamp_ns)— Per-episode close-out pushed to sinks atepisode_end. (L119)class DatasetSink(Protocol)— Fan-out target withopen_episode/write_frame/close_episode/finalize. (L137)open_episode(self, header) -> None(L151) — Begin a new episode. Idempotent only within one episode.write_frame(self, frame) -> None(L154) — Append one frame to the current episode.close_episode(self, summary) -> None(L157) — Finalise the current episode (success flag, frame count).finalize(self) -> None(L160) — Flush pending I/O and release resources. Idempotent.class RolloutRecorder(*, robot, task_string, fps, sinks, repo_id=None)— In-memory accumulator that fans every step out to one or moreDatasetSinkimplementations and writes the OTelopenral.dataset.repo_id/episode_idx/frame_idxattributes on the activerskill.tickspan. (L164)episode_start(*, task_string=None) -> int— Open a new episode; returns its idx. (L293)record_frame(*, observation_state, images, action, reward, terminated, truncated, stamp_ns, trace_id=None, span_id=None) -> int— Append one frame. Captures the activerskill.tickspan's(trace_id, span_id)onto the frame (ISSUE-109); explicittrace_id/span_idoverride the live capture (the offline converter replays the bag's original ids). (L338)episode_end(*, success: bool) -> EpisodeSummary— Close the current episode. (L451)finalize() -> None— Flush all sinks idempotently. (L484)prop fps, robot_name, repo_id, n_sinks, expected_state_shape(L219–239) — Read-only views consumed by callers building the per-frame payload (robot_nameL224,repo_idL229,n_sinksL234,expected_state_shapeL239).expected_image_keys() -> tuple[str, ...]— Camera keys (withoutobservation.images.prefix) the sinks expect; derived fromRobotDescription.sensors[*].vla_feature_key. (L253)
python/dataset/src/openral_dataset/schema_map.py
Pure RobotDescription → LeRobot v3 features dict mapping; no I/O, no lerobot import.
- module constant
_LEROBOT_IMAGE_DTYPE: str = "video"(L31) - module constant
_LEROBOT_STATE_DTYPE: str = "float32"(L32) - module constant
_LEROBOT_ACTION_DTYPE: str = "float32"(L33) — LeRobot v3 feature dtypesfeatures_from_robotassigns. - module constant
_IMAGE_MODALITIES: frozenset[str](L38) —{"rgb", "depth", "rgbd", "thermal", "ir"}, sensor modalities mapped to image features. @dataclass class FeatureSpec(key, dtype, shape)— Decoupled feature descriptor; sinks translate to lerobot's{'dtype', 'shape', 'names'}format. (L42)features_from_robot(robot: RobotDescription, *, fps: float) -> dict[str, FeatureSpec]— Build the LeRobot v3 features dict for the recorder. ReadsObservationSpec.state_shape,ActionSpec.dim, andSensorSpec.vla_feature_key(image modalities only) from the robot manifest. (L59)
python/dataset/src/openral_dataset/bag.py
Mcap-backed DatasetSink for online hardware recording.
- module constant
TOPIC_TICK: str = "/openral/tick"(L53) - module constant
TOPIC_EPISODE: str = "/openral/episode"(L54) - module constant
TOPIC_IMAGE: str = "/openral/dataset/image"(L59) — the three mcap topic names the converter imports by symbol. - module constant
_TICK_SCHEMA: dict[str, Any](L66) — JSON Schema for the/openral/tickmessage. - module constant
_IMAGE_SCHEMA: dict[str, Any](L99) — JSON Schema for the/openral/dataset/imagemessage. - module constant
_EPISODE_SCHEMA: dict[str, Any](L125) — JSON Schema for the/openral/episodemessage. - module constant
PHASE_START: int = 0(L139) - module constant
PHASE_END: int = 1(L140) — the twoEpisode.phaseenum values. - module constant
_NDIM_H: int = 1(L144) - module constant
_NDIM_HW: int = 2(L145) - module constant
_NDIM_HWC: int = 3(L146) — expectedndarray.ndimfor a mono/2D/HWC image frame. - module constant
_QUEUE_MAXSIZE: int = 1024(L152) — bound on the writer thread'squeue.Queue;write_framedrops (counted inn_dropped) rather than block the hot path when full. - module constant
_WRITER_JOIN_TIMEOUT_S: float = 5.0(L157) —finalize's join timeout on the writer thread. - module constant
_MCAP_INSTALL_HINT: str(L161) — install hint raised whenmcapis not importable. - module constant
_STOP(L173) — sentinel_Stop()instance enqueued to tell the writer thread to exit. class Rosbag2Sink(DatasetSink)— Writes everyRolloutRecorderevent into an mcap file readable byros2 bag info/ Foxglove / mcap-cli. Daemon writer thread + bounded queue sowrite_framenever blocks on disk I/O. Topics:/openral/tick(metadata plus inlineobservation_state/actionarrays),/openral/episode(phase markers),/openral/dataset/image(one frame per camera per tick) — self-sufficient for conversion, no separate topic join needed. (L176)bag_path(self) -> Path(L251) — Output bag path (read-only).n_ticks_written(self) -> int(L256) — Number of/openral/tickmessages successfully written.n_episode_markers_written(self) -> int(L261) — Number of/openral/episodemessages (start + end) written.n_images_written(self) -> int(L266) — Number of/openral/dataset/imagemessages successfully written.n_dropped(self) -> int(L271) — Number of messages dropped because the writer queue was full.open_episode(self, header) -> None(L277) — Open the bag on first call; emit Episode(PHASE_START).write_frame(self, frame) -> None(L291) — Enqueue a Tick message (incl. inlineobservation_state/action+ the frame'strace_id/span_id, ISSUE-109) and one DatasetImage message per camera; never blocks. The off-thread mcap write reads the ids off the frame because the OTel context is gone by then.close_episode(self, summary) -> None(L334) — Emit Episode(PHASE_END) with success flag.finalize(self) -> None(L344) — Drain queue, stop writer thread, close mcap. Idempotent.
python/dataset/src/openral_dataset/converter.py
Offline mcap rosbag2 → LeRobotDataset v3 converter.
- module constant
_MCAP_INSTALL_HINT: str(L49) — install hint raised whenmcapis not importable. @dataclass class DatasetSummary(output_root, n_episodes, n_frames, n_success, repo_id)— Returned byfrom_bagdescribing what landed on disk. (L56)class Rosbag2ToLeRobotConverter(L84) — Offline replay of aRosbag2Sink-produced mcap into a LeRobotDataset v3.Rosbag2ToLeRobotConverter.from_bag(*, bag_path, robot, output_root, repo_id=None, license="CC-BY-4.0", fps=None) -> DatasetSummary— Walk aRosbag2Sink-produced mcap, group ticks under episode markers, join each tick's inline arrays + camera frames, and replay each episode through a realLeRobotDatasetSinkto produce a reloadable v3 dataset with real proprio/action/video. Each replayed tick re-injects the bag's original(trace_id, span_id)so the on-disk frame points at the source rollout, not the convert run. RaisesROSConfigErroron a missing bag, missing episode markers, or a mismatched robot. (L104)
python/dataset/src/openral_dataset/frame_trace.py
ISSUE-109 — pivot a written LeRobotDataset frame back to its OTel ids.
read_frame_trace(*, root, episode_idx, frame_idx) -> tuple[str, str]— Return the(trace_id, span_id)stamped on a v3 frame. Reads theroot/data/**/*.parquetcorrelation columns directly viapyarrow(no video decode), so it works without a torchcodec/ffmpeg backend. RaisesROSConfigErrorwhen the root has no parquet, the dataset predates the columns, or no(episode_idx, frame_idx)row matches. Backsopenral replay --frame. (L28)
python/dataset/src/openral_dataset/sinks.py
LeRobotDataset v3.0 (codebase_version="3.0") writer; deferred LeRobotDataset.create so per-camera shapes come from the first frame.
- module constant
DEFAULT_LICENSE: str = "CC-BY-4.0"(L43) - module constant
DEFAULT_VCODEC: str = "libsvtav1"(L48) — default license / video codec for a newLeRobotDatasetSink. - module constant
_FPS_INTEGER_TOLERANCE: float = 1e-6(L51) — tolerance for treating a recorded fps as an integer. - module constant
_TRACE_FEATURES: dict[str, dict[str, Any]](L59) — thetrace_id/span_idstring-feature specs added to every LeRobot v3 dataset (ISSUE-109). - module constant
_LEROBOT_INSTALL_HINT: str(L67) — install hint raised whenlerobot>=0.5.1is not importable. class LeRobotDatasetSink(DatasetSink)— Implementation ofDatasetSinkwriting LeRobot v3 datasets via reallerobot.datasets.LeRobotDataset.create / add_frame / save_episode / finalize. Lazy-imports lerobot at construction. (L85)__init__(*, root, robot, fps, repo_id=None, license="CC-BY-4.0", vcodec="libsvtav1")— RaisesROSConfigErrorif lerobot ≥ 0.5.1 is not importable. (L120)open_episode(header) -> None— Stash the task string for per-frame tagging. (L248)write_frame(frame) -> None— Validates per-frame shapes against the declared features, then forwards toLeRobotDataset.add_frame. Adds the frame'strace_id/span_idasstringparquet columns (ISSUE-109). (L267)close_episode(summary) -> None— CallsLeRobotDataset.save_episode(parallel_encoding=True)and accumulates the per-dataset success counter. (L344)finalize() -> None— CallsLeRobotDataset.finalize()then appendsdataset_success_rate/license/repo_idand the dataset-leveltrace_ids/n_traces(distinct OTel traces, ISSUE-109) tometa/info.json["metadata"], and writes the per-episodeepisode_index → trace_idmap to themeta/openral_traces.jsonsidecar. (L385)
python/observability/src/openral_observability/replay/bag_reader.py
Read a rosbag2 (mcap) recording as a stream of BagMessage for the F7 bag↔OTel replay correlator.
- module constant
_TRACEPARENT_RE_BYTES(L40) —re.compile(rb"00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})"), matches atraceparentembedded in a raw CDR payload. - module constant
_TRACEPARENT_RE_STR(L43) — same pattern,strform, for JSON-schema-encoded payloads. - module constant
_RAW_TRACE_ID_RE(L48) —re.compile(r"^[0-9a-f]{32}$"). - module constant
_RAW_SPAN_ID_RE(L49) —re.compile(r"^[0-9a-f]{16}$"). class BagMessage(L84) — One rosbag2 record surfaced to the bag↔OTel replay correlator; fieldstopic,log_time_ns,publish_time_ns,trace_id,traceparent,schema_name,payload_summary._trace_id_from_json_payload(payload) -> tuple[str, str](L52) — Return(trace_id_hex, traceparent)from ajsonschemapayload._resolve_mcap_path(path) -> Path(L117) — Find the actual.mcapfile inside a rosbag2 directory._extract_traceparent_from_ros2msg(payload) -> tuple[str, str](L135) — Return(trace_id_hex, traceparent)from a CDR payload, or empties.read_bag(bag_path) -> Iterator[BagMessage](L144) — Yield every message inbag_pathas aBagMessage.
python/observability/src/openral_observability/replay/correlator.py
Merge a rosbag2 message stream with dashboard-indexed OTel spans into one sorted timeline for openral replay.
class TimelineEntry(L27) — One row in the correlated timeline; fieldskind: Literal["bag", "span"],ts_ns,trace_id,topic,span_name,attrs,duration_ms.to_json(self) -> dict[str, Any](L55) — Return a plain-dict view suitable forjson.dumps.list_bag_trace_ids(bag_messages) -> list[dict[str, Any]](L68) — Return distinct trace_ids inbag_messageswith their per-trace counts.build_timeline(bag_messages, spans, *, trace_id=None) -> list[TimelineEntry](L95) — Mergebag_messages+spansinto one sorted timeline.
python/observability/src/openral_observability/replay/trace_query.py
Minimal read-only HTTP client for the dashboard's F7 trace-query endpoints (GET /api/traces, GET /api/spans/{trace_id}).
- module constant
_HTTP_NOT_FOUND: int = 404(L23) class TraceQueryError(RuntimeError)(L26) — Raised when the dashboard does not return a usable JSON body.class DashboardTraceClient(L31) — Minimal read-only HTTP client for the dashboard trace endpoints.base_url: str = "http://127.0.0.1:8000",timeout_s: float = 5.0.list_traces(self) -> list[dict[str, Any]](L45) — ReturnGET /api/tracesdecoded, sorted server-side most-recent first.get_spans(self, trace_id) -> list[dict[str, Any]](L54) — Return every span fortrace_id; empty list when not indexed.
python/observability/src/openral_observability/replay/cli.py
openral replay implementation: record a bag with the right topics, then join it against dashboard-indexed OTel spans by trace_id.
- module constant
ProfileName(L38) —Literal["slim", "full"], the tworos2 bag recordtopic/regex profiles. - module constant
RECORD_PROFILES: dict[str, dict[str, list[str]]](L52) — theslim(safety/estop/world-state-slow + compressed camera images) andfull(adds world-state-fast, joint_states, tf, all perception, every camera stream, and — bytopic_types— everyLaserScan/PointCloud2/Imutopic whatever a manifest names it) topic + regex + topic-type lists; camera regexes are built fromre.escape(CAMERA_TOPIC_PREFIX). build_record_command(*, profile, output_dir, storage="mcap", extra_topics=(), extra_regex=()) -> list[str](L102) — Compose theros2 bag recordargv forprofile. Verbatim topics are folded into the--regexas anchored patterns, never passed as explicit topics: rosbag2 (Jazzy) stops discovery once as many topics are subscribed as were listed explicitly, so mixing the two silently dropped later topics.topic_typesgo to--topic-types.run_record(*, profile, output_dir, storage="mcap", extra_topics=(), extra_regex=(), dry_run=False) -> tuple[list[str], subprocess.CompletedProcess[bytes] | None](L233) — Invokeros2 bag recordwith the chosen profile.class ReplayResult(L156) — Output ofrun_replay— both summary + the joined timeline; fieldstrace_id,bag_trace_ids,timeline,bag_path.to_json(self) -> dict[str, Any](L175) — Return a plain-dict view suitable forjson.dumps.run_replay(*, bag_path, trace_id, dashboard_url) -> ReplayResult(L185) — Readbag_path, fetch matching spans, return the joined timeline.write_timeline(result, out_path) -> None(L306) — Persist aReplayResultas pretty-printed JSON toout_path.