Skip to main content
Glama

TopicForge

PyPI version CI Python versions License: MIT Read-only

A read-only MCP (Model Context Protocol) server that lets an AI agent inspect a ROS2 graph, recorded bag files and the DDS layer underneath ROS. The code has no write path: it cannot publish to the bus or command a robot, and there is no permission system to configure.

Every tool also declares itself read-only in the MCP protocol (readOnlyHint, destructiveHint=false, idempotentHint, with an honest openWorldHint), and a test fails if any tool lacks it. See SECURITY.md for the guarantee and its limits.

It gives the agent fourteen typed tools that return frozen Pydantic schemas (output contract 2: one object per tool, one note, units in the field names; see docs/CONTRACT.md), identical whether the server talks to a real robot or to its built-in mock fixtures. Ask why nav_planner gets no scan, and the agent reads the bus, finds the BEST_EFFORT writer facing a RELIABLE reader and names the incompatible policy (see examples/02-debug-qos-mismatch.md). It is meant for ROS2 developers, robotics ML/CV engineers and teams that cannot accept a write path into a production stack.

For DDS, TopicForge joins a domain as a read-only participant through one open-source binding (Eclipse CycloneDDS from PyPI) and reads the builtin discovery topics that the OMG DDS-RTPS protocol standardizes. Cyclone DDS, Fast DDS and Dust DDS participants have been observed on live buses: the Fast DDS ones on every CI run of the ROS 2 test bench (Humble and Jazzy with rmw_fastrtps_cpp) and in the OmniSim validation. RTI Connext, OpenDDS and CoreDX announce themselves through the same standard discovery, but none of them has been observed yet. This covers discovery only: participants, readers, writers and their QoS. See docs/dds-interop-matrix.md.

Quickstart

No ROS2 is needed; the mock adapter serves deterministic fixtures for a small differential robot (LIDAR + RGB camera). Python 3.10 to 3.13.

pip install topicforge
TOPICFORGE_MODE=mock python -m topicforge
# Windows PowerShell: $env:TOPICFORGE_MODE="mock"; python -m topicforge

The server speaks MCP over stdio and waits for a client, so wire it into one. For Claude Desktop, add to claude_desktop_config.json:

{
  "mcpServers": {
    "topicforge": {
      "command": "python",
      "args": ["-m", "topicforge"],
      "env": { "TOPICFORGE_MODE": "auto" }
    }
  }
}

Then ask it to list the topics or to analyze /tmp/demo.mcap. For Claude Code: claude mcp add topicforge -- topicforge. If the client cannot launch the server (WSL2 to Windows, a robot over an SSH tunnel), topicforge --transport streamable-http --port 8765 serves http://127.0.0.1:8765/mcp on loopback only; see docs/CLIENTS.md.

Other MCP clients. Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in docs/CLIENTS.md. One-click: Add to Cursor | Install in VS Code.

Setup for a real ROS2 environment (WSL2, Linux, Docker, native Windows) is in docs/TESTING.md; recurring monitoring prompts and the privacy contract are in docs/TUTORIEL.md.

Related MCP server: ros2-mcp

Tools

Every response except health_check carries mode_effective ("live" or "mock"), so a caller can tell a real graph from fixtures, and a single note that explains anything unusual. health_check.contract_version is 2. The full reference, with every field, is generated in docs/TOOLS.md; upgrading from 0.6.x: docs/MIGRATION-0.7.md.

Tool

Purpose

health_check

Environment and mode introspection. Always succeeds; reports mode next to requested_mode

list_topics

Discover the ROS2 graph

get_topic_info

Message type, publisher/subscriber counts and QoS for one topic

list_nodes

ROS 2 nodes on the graph: names, namespaces, duplicate names (one ros2 node list call)

get_node_info

One node's publishers, subscribers, services, actions, parameters (secrets masked) and use_sim_time; a node that does not answer is reported, not an error

sample_messages

Peek recent messages on a ROS2 topic (count capped at 50)

analyze_bag

Summarize a .mcap / .db3 recording or rosbag2_* directory (via ros2 bag info)

list_participants

DDS participants on the domain: vendor, name (EntityName QoS, Cyclone) and hostname

detect_qos_mismatches

Incompatible QoS pairs between DDS readers and writers

peek_dds_samples

Raw DDS samples; structured on the three builtin discovery topics, presence-only on user topics

participant_events

Timeline of participant discovered / lost events

topic_metrics

Frequency, sequence-gap and latency schema; data only for builtin discovery topics

peek_bag_samples

Decoded samples from a recorded bag, including ROS 1 .bag (rosbags ships with the package)

list_endpoints

DDS writers and readers with structured QoS, per-topic roll-up that flags orphans (writer with no reader, reader with no writer)

The server also offers two MCP prompts, diagnose-dds-bus and inspect-ros2-robot, that give a client the read-only call order, and short server instructions for clients that do not show prompts (see docs/CLIENTS.md).

Walkthroughs against the mock, each with the exact tool calls and payloads, are in examples/. To run the DDS tools against a real bus with several programs and vendors, see examples/dds/README.md (python examples/dds/run_all.py).

Modes

Mode

When to use

Backend

mock

Development, demos, CI, screencasts

Deterministic fixtures

live

ROS2 sourced and on PATH, and/or a DDS backend

ros2 CLI, DDS participant

auto

Detect what is available, else mock (default)

Best available

live and auto degrade instead of failing: if neither ros2 nor a DDS backend comes up, the server serves the mock fixtures and health_check reports mode: "mock" next to requested_mode. Only TOPICFORGE_MODE=mock forces fixtures unconditionally. The live ROS2 adapter shells out to the ros2 CLI, so rclpy does not need to be importable.

DDS backends

pip install topicforge                           # includes Eclipse CycloneDDS where wheels exist (Python 3.10-3.13; Linux x86_64, Windows x64, macOS)
pip install "topicforge[dds]"                    # alias, kept so older commands work; forces a source build elsewhere (needs the Cyclone C library)
TOPICFORGE_DDS_BACKEND=cyclone python -m topicforge

TOPICFORGE_DDS_BACKEND accepts mock (default), cyclone, fast and auto (fast, then cyclone, then mock, whichever binding imports). The default mock selects no DDS backend: installing the Cyclone binding is not enough, you must also set TOPICFORGE_DDS_BACKEND=cyclone. With TOPICFORGE_MODE=live and no backend selected, the DDS tools raise DDS module is not active: ... with the actual cause (backend not selected, binding not installed, or binding installed but the adapter failed to start), and health_check reports dds_backend: "none" plus dds_inactive_note. An explicit value is honoured with or without ros2 on PATH, in any mode except mock. If the binding is missing or the participant cannot start, the server logs a warning naming the cause and falls back to the ROS2 CLI alone, or to the mock fixtures. When both ros2 and a DDS backend are up, a composite adapter routes the six ros2 CLI tools (list_topics, get_topic_info, list_nodes, get_node_info, sample_messages, analyze_bag) to the CLI and the six DDS tools to the DDS backend; peek_bag_samples reads bags without the CLI.

A Fast DDS adapter exists but has never run against a bus, and its fastdds Python binding is not on PyPI: build it from eProsima's sources and install it next to TopicForge. There is no [dds-fast] extra. Any other value (rti, opensplice, coredx, intercom, opendds, dust) is rejected at startup with an error that lists the valid ones; Cyclone already sees those vendors' participants through standard discovery. Full backend selection, the routing table and the QoS mismatch scenario are in docs/DDS_QUICKSTART.md; error messages are in docs/TROUBLESHOOTING.md.

Configuration reference

Variable

Default

Description

TOPICFORGE_MODE

auto

mock, live or auto

TOPICFORGE_LOG_LEVEL

INFO

DEBUG, INFO, WARNING, ERROR

TOPICFORGE_ROS2_BIN

ros2

Name or path of the ROS2 CLI binary

TOPICFORGE_TELEMETRY

off

Opt-in anonymous telemetry; an unrecognized value aborts startup. See Telemetry

TOPICFORGE_DDS_BACKEND

mock

mock (no DDS backend), cyclone, fast, auto; anything else is rejected

TOPICFORGE_DDS_DOMAIN_ID

0

DDS domain observed (0..232). Joined at startup; changing it needs a restart

TOPICFORGE_MAX_SAMPLE_BYTES

1048576

Size cap for one sampled message (1 KiB..64 MiB); a call returns at most 4 times that. Over-cap messages are dropped with a note

Command-line options (not environment variables): --transport stdio|streamable-http (default stdio) and --port N (default 8765, only with streamable-http). The HTTP transport binds 127.0.0.1 only, with DNS-rebinding protection and no authentication; see docs/CLIENTS.md.

Samples with comments are in .env.example. Any invalid value stops the server with topicforge: configuration error: ... and exit code 2, so a typo cannot silently change behaviour.

Limitations

External validation against a simulated robot's ground truth (OmniSim): docs/VALIDATION.md.

  • DDS validation is partial. The Cyclone adapter has run against real buses with Cyclone, Fast DDS and Dust DDS participants, on Windows and in CI on Ubuntu and Windows (.github/workflows/demo.yml, .github/workflows/ros2-live.yml). The Fast DDS adapter (TopicForge joining through the fastdds binding) has never run against a bus, and no RTI, OpenDDS, CoreDX or OpenSplice participant has been observed by this project. The multi-vendor claim rests on the RTPS protocol guarantee, not on a recorded cross-vendor run.

  • User-topic payloads are not decoded. peek_dds_samples on a user topic returns count 0 and a note that the topic is announced on the bus; no traffic is read. topic_metrics therefore has data only for the builtin discovery topics and says so in its status. It is a discovery-layer probe, not a publish-rate monitor.

  • Liveliness at runtime is not observed. A writer that is alive but silent (a hung process whose lease is still renewed) looks healthy, because TopicForge reads discovery, not data. An opt-in data probe is planned. A crash and a clean leave cannot be told apart, and lost_ns is an upper bound of the death.

  • Cyclone vendor ids: participants that do not follow the RTPS vendor-id convention in their GUID prefix (Dust DDS, and RTI by default) are reported with vendor unknown.

  • Single domain: the server observes the domain it joined at startup; changing it needs a restart.

  • DDS Security is not handled. A participant without credentials sees an empty secure bus. detect_qos_mismatches checks Partition, type name, Reliability, Durability, Deadline, Liveliness, LatencyBudget, Ownership (kind), DestinationOrder and DataRepresentation (History as a risk); Presentation, XTypes assignability and runtime behavior are not checked, and the result lists them in policies_unchecked. It returns a MismatchScan envelope: read reports for the mismatches.

  • Fast DDS serves no list_endpoints.

  • sample_messages (live) streams ros2 topic echo until count messages arrive or timeout_s (1..40, default 10, for the whole call) runs out, and returns what arrived with a note (N of M messages within T s, saying whether a publisher exists). QoS is matched to the publishers, so latched topics work. The payload has nested named fields (payload.header.stamp.sec); timestamp_ns is header.stamp (the publisher's clock, sim time on a simulation), the time in the body of /clock, /tf and /rosout messages (stamp_source payload), or 0 for other headerless types, with stamp_source and received_ns (wall clock when the CLI printed it). Arrays are cut at 128 elements by default; max_array_length (1..65536, or null for no cut) and arrays_summary_only change that, and a cut is listed under _truncated_fields. nan and inf come back as strings. count above 50 is capped with a note.

  • analyze_bag (live) parses ros2 bag info text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates ((n - 1) / span) and latched are added when the bag can be read locally (.db3 with the standard library, .mcap with rosbags), else rates fall back to count / bag duration (frequency_basis). peek_bag_samples reads the file itself, through rosbags, returns the first count messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its timestamp_ns is the message's header.stamp (stamp_source header), the body time of Clock, TFMessage and Log messages (payload) or, without either, the bag record time (stamp_source recorded), and recorded_ns is always the bag record time; bags that embed no message definitions (Humble .db3) are decoded with the Humble definitions, or the distro the bag records, and note says so. Without ros2, bag tools return fixtures: check health_check for mode: "mock" before trusting bag output. health_check also reports sim_clock_published (live only): true when /clock has a publisher, a hint that header stamps may be simulation time.

  • Calls are serialized per backend (one ros2 call at a time, one DDS call at a time) and each call has a 45 s wall budget, lock wait included; a call that cannot get its lane fails at once with busy. health_check and peek_bag_samples take no lock.

  • No streaming or push subscriptions: tools are strictly request/response.

Next: an opt-in probe to tell a hung writer from a healthy one, wider real-bus validation (Fast DDS, RTI, OpenDDS), and DDS Security. Open work is tracked in issues.

Telemetry

Opt-in, anonymous and off by default. When off, instrumentation returns the handler unchanged: no event is built, no transport is constructed, no network code runs (pinned by tests/test_telemetry.py::test_build_app_off_makes_no_transport_calls).

TOPICFORGE_TELEMETRY=on python -m topicforge

On-values: on, 1, true, yes, enabled. Off-values: unset, off, 0, false, no, disabled. Anything else is a configuration error rather than a silent "off".

When on, each tool call emits one event with exactly six fields:

Field

Example

Notes

tool_name

"list_topics"

One of the fourteen tools, never argument values

latency_ms

12.34

Handler wall-clock duration, 2 decimals

mode

"mock"

Mode of the adapter actually serving: mock or live

version

"0.7.1"

TopicForge server version

session_id

"a1b2c3..."

Random UUID per process, never persisted

success

true

Whether the handler returned or raised

Never sent: topic names, message types or payloads, bag paths or contents, hostnames, usernames, IP addresses, environment variables, error messages. The field set is fenced by tests/test_telemetry.py::test_payload_contains_only_whitelisted_keys; adding a field requires updating this section. The default transport is a structured log line; there is no HTTP endpoint yet. The implementation is in src/topicforge/telemetry/.

Security model

TopicForge is designed for local trust: it runs as a subprocess of your MCP client on a machine you control and inspects your own ROS2 graph, DDS domain and bag files. It is not hardened for adversarial inputs.

  • TOPICFORGE_ROS2_BIN accepts an arbitrary path; treat it the way you treat PATH.

  • analyze_bag and peek_bag_samples open whatever path the client passes (no workspace isolation, no symlink restriction).

  • All ros2 invocations use subprocess.run with an argument list, never shell=True. ROS2 topic names are validated against ^/[A-Za-z0-9_/]+$ first.

  • The server loads no third-party code at startup.

  • No outbound network calls unless telemetry is turned on.

Before exposing TopicForge to untrusted MCP clients (hosted endpoints, shared environments), add path isolation and revisit the TOPICFORGE_ROS2_BIN policy. Vulnerability reports: see SECURITY.md.

Development

git clone https://github.com/yaniswav/TopicForge.git && cd TopicForge
python -m venv .venv && source .venv/bin/activate     # Windows: .venv\Scripts\Activate.ps1
pip install -e ".[dev]"
python -m ruff check src tests
python -m pytest

Tests run against the mock adapter, the live adapter's pure parsers and the binding-free DDS helpers; they never need a running ROS graph. Tests needing the cyclonedds or fastdds binding skip themselves when it is absent, and the integration marker (real-bus tests) is deselected by default. The Makefile (make check) uses POSIX shell syntax; on plain PowerShell run the commands above. See CONTRIBUTING.md.

Upgrading

TopicForge is pre-1.0; CHANGELOG.md lists every change, including the yanked releases and removed extras. 0.7.0 changed the shape of tool results (contract_version 2): docs/MIGRATION-0.7.md maps every old field to its new one.

Layout

src/topicforge/
  server/      MCP bootstrap, build_app(settings), prompts and instructions
  tools/       thin MCPServer handlers, no backend logic
  services/    input validation, orchestration, adapter factory
  adapters/    ros2_live, ros2_mock, dds_cyclone, dds_fast, common/ (binding-free logic)
  models/      frozen Pydantic schemas, the contract with MCP clients
  config/      settings and mode resolution
  telemetry/   opt-in, off by default
examples/      mock walkthroughs (*.md) and runnable live DDS examples (dds/)
scripts/       real-bus interop checks
docs/          guides, product plan
tests/         pytest suite, mock-only, no ROS2 or DDS required

Layers are strictly separated: handlers never call subprocess, adapters are the only code that talks to a backend, and new backends implement the MiddlewareAdapter protocol in adapters/base.py.

License

MIT, see LICENSE. Integration or support work for a specific ROS 2 / DDS setup: ethvignot.yanis@gmail.com.

Available Tools

14 tools
analyze_bagA
Read-onlyIdempotent

Summarize a ROS 2 bag at path as a BagAnalysis: storage format, duration, message count, per-topic stats and mode_effective. Per topic frequency_hz is (n - 1) / (last - first message time) (frequency_basis topic_span), with first_timestamp_ns, last_timestamp_ns and latched; a latched topic whose messages all fall within 1 s (e.g. /tf_static) has a null frequency_hz. When the bag cannot be read locally or is a .mcap over 200 MiB, the rate falls back to count / bag duration (bag_duration) and note says why. Live mode runs ros2 bag info on .mcap, .db3 or rosbag2_* directories (for ROS 1 .bag use peek_bag_samples); mock mode returns fixture data for any bag-like path, and only mock detects anomalies. Raises an MCP error for a malformed or missing path (live), unparseable output, or without a ros2 CLI. Read-only; no side effects. Duration: runs the ros2 CLI behind the ROS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another ros2 call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYesBag path: a `.mcap` or `.db3` file or a `rosbag2_*` directory (`peek_bag_samples` also reads ROS 1 `.bag`; `analyze_bag` does not). Existence and format are checked live; mock accepts any well-formed path.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoWhy the result is less detailed than usual (e.g. rates over the whole bag duration); else `None`.
pathYesBag path as supplied.
topicsYesPer-topic statistics for every topic present in the bag.
anomaliesNoNotes on gaps or clock jumps; mock mode only.
bag_formatNoDetected container: `mcap`, `db3`, `bag` or `unknown`; `None` for `ros2 bag info` text.
duration_sYesBag duration in seconds.
message_countYesTotal number of messages across all recorded topics.
mode_effectiveYes`live` or `mock` (fixtures).
storage_formatNo`mcap`, `sqlite3`, or other storage identifier when known.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover the safety profile (readOnly/idempotent/non-destructive), and the description goes well beyond them: the 45 s budget, exclusive ROS lock behavior, immediate `busy` error with retry, error conditions, and the mode-dependent anomaly detection. This is exactly the extra context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Core purpose and the sibling pointer are front-loaded, and the dense detail (frequency formula, fallback, modes, errors, duration) is organized with bold lead-ins. It is long, but nearly every sentence carries operational information; a minor trim of the formula detail is possible.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite an output schema existing, the description explains the non-obvious return semantics (`mode_effective`, `frequency_basis`, latched-topic null `frequency_hz`, `note` fallback) that an agent needs to interpret results. Combined with error and timing disclosure, nothing needed to invoke it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% for the single `path` parameter, and the schema already documents the accepted formats and the ROS 1 exclusion, so the baseline of 3 applies. The description reinforces format acceptance but adds little parameter meaning beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('Summarize a ROS 2 bag at `path`') and names the return type `BagAnalysis` with its fields. Explicitly distinguishes itself from the sibling `peek_bag_samples` for ROS 1 `.bag`, so an agent can separate the two without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit routing rules: live mode for `.mcap`/`.db3`/`rosbag2_*`, direct the agent to `peek_bag_samples` for ROS 1 `.bag`, and mock mode semantics. It also names the failure conditions (malformed/missing path, unparseable output, missing `ros2` CLI) and the busy/retry case, which is complete when-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

detect_qos_mismatchesA
Read-onlyIdempotent

Explain why DDS readers and writers on a topic do not talk, and who will. Pairs every reader with every writer per topic and returns a MismatchScan: summary (one line, read it first), matched (pairs DDS will connect given the announced QoS; data flow is not observed), reports (incompatible or risky QoS pairs, with participant names, requested vs offered values and the failed rule in details), not_matched (pairs DDS never matches: different partitions or type names; the QoS rules are NOT evaluated for them, and latent_incompatible_policies lists what would also break once fixed), hints (probable typos in orphan topic names, type id notes), plus pairs_checked, topics_scanned, policies_checked and policies_unchecked. Checked: Partition (* and ? wildcards), type name, Reliability, Durability, Deadline, Liveliness, LatencyBudget, Ownership, DestinationOrder, DataRepresentation, History (risky only, where announced). An empty reports with a non-empty not_matched still means no data flows, and an all-empty result does not prove the bus healthy: discovery shows the QoS DECLARED, not runtime behavior (a 'deadline missed' log with compatible QoS means the writer's real period exceeds the deadline, which TopicForge cannot see). Pass topic to scope the scan. The lists are capped at 200 each (truncated; *_total keep the counts). A late_joiner match is a VOLATILE writer whose reader joined later on the same host: normal. Reports and partition splits carry suggested_fixes: text for a human, never applied by TopicForge; verify_with names the call that confirms the repair. Read-only by architecture. Raises an MCP error when no DDS module is active; mock returns fixtures. Duration: uses the DDS binding behind the DDS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another DDS call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault
topicNoOptional DDS topic to scope the scan to: `scan` or `rt/scan`. Omit to scan all topics.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoOne sentence of context, or `None`.
hintsYesLeads that are not findings: probable typos, type id notes, incomplete checks.
matchedNoPairs DDS will connect given the announced QoS (data flow is not observed).
reportsYesPairs sharing partition and type with incompatible or risky QoS.
summaryNoOne-line reading of the scan: counts of incompatible, not matched and matched pairs.
truncatedNoTrue when `reports`, `matched` or `not_matched` hit the 200-entry cap (see `*_total`); narrow with `topic`.
not_matchedYesPairs DDS never matches (partition or type name). Non-empty with empty `reports` is NOT all-clear: no data flows between them.
matched_totalNoMatched pairs before the size cap.
pairs_checkedYesSame-topic (reader, writer) pairs examined.
reports_totalNoReports before the size cap.
mode_effectiveYes`live` or `mock` (fixtures).
topics_scannedYesTopics that had at least one endpoint in scope.
policies_checkedYesPolicies compared on every pair.
not_matched_totalNoNot-matched pairs before the size cap.
policies_uncheckedYesWhat the scan does not cover, each with a reason; a clean result says nothing about them.

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Far exceeds the annotations: it discloses the 45 s duration and lock semantics, the busy error and retry behavior, the MCP error when no DDS module is active, mock fixtures, the 200-item truncation caps, and the crucial caveat that an empty result does not prove bus health. Read-only status is reinforced and no behavioral trait is left implicit.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the outcome and read-this-first `summary` guidance, which is strong. However, the description spends many clauses enumerating return fields of a `MismatchScan` even though an output schema already exists, making it longer than necessary.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity and the interpretive traps (not_matched vs empty reports, late_joiner being normal, suggested_fixes not applied), the description supplies everything needed to interpret results correctly, with the output schema freeing it from raw field-type explanation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Only one parameter with 100% schema description coverage, so the schema already documents `topic` including its 'scan'/'rt/scan' examples. The description's 'Pass `topic` to scope the scan' restates the schema without adding format or edge-case meaning, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: explaining why DDS readers/writers on a topic fail to communicate, and pairing readers with writers per topic. This is clearly distinguishable from siblings like list_topics or get_topic_info, which inventory rather than diagnose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear usage context (diagnosing QoS-driven connection failures, scoping with `topic`, retrying on a `busy` error) but never explicitly contrasts this with sibling diagnostics such as get_topic_info or health_check. No 'use this instead of X when Y' routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_node_infoA
Read-onlyIdempotent

ROS 2 graph only; on a DDS-only setup use list_participants. Describe one ROS 2 node as a NodeInfo: the topics it publishes and subscribes to, the services and actions it serves or calls (each {name, type}), its parameters and use_sim_time. Interfaces come from the graph (ros2 node info), which never waits for the node. Parameters come from a read (ros2 param dump) bounded by timeout_s: a node that does not answer in time is a finding, not an error: parameters is null, parameters_note says its executor is probably blocked, and the interfaces are still returned. A value whose name contains password, secret, token, api_key or credential is replaced by <masked> and a long value is cut (truncated, original_size), both said in parameters_note. With several nodes of that name duplicate_count is above 1 and note warns. Raises an MCP error when the node is not on the graph (the message lists close matches), the name is malformed, or without a ros2 CLI. Read-only: the only request TopicForge ever sends is a parameter read (list/get); it sends no other request to a node. Duration: runs the ros2 CLI behind the ROS lock, one call at a time: up to three CLI calls (node list and node info, 8 s each at most; then the parameter read, at most timeout_s, 20 s). The whole call, lock wait included, ends within 45 s: if another ros2 call holds the lock it fails at once with a busy error (retry), and a call that waited has less time for the parameter read.

ParametersJSON Schema
NameRequiredDescriptionDefault
nodeYesFully qualified node name as `list_nodes` gives it, e.g. `/robot1/lidar_driver` (a missing leading `/` is added).
timeout_sNoSeconds to wait for the parameter read, 1..20, default 8. A node that does not answer is reported in `parameters_note`, not as an error.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoOne sentence of context, or `None`.
full_nameYesFully qualified node name, e.g. `/lidar_driver`.
parametersNoParameters sorted by name; `None` when unreadable (see `parameters_note`); an empty list means none.
publishersYesTopics the node publishes.
subscribersYesTopics the node subscribes to.
use_sim_timeNoValue of the node's `use_sim_time` parameter. `None` when unknown: see `use_sim_time_note`.
action_clientsYesActions the node calls.
action_serversYesActions the node serves.
mode_effectiveYes`live` or `mock` (fixtures).
duplicate_countYesNodes using this exact full name; above 1, the data above belongs to one of them.
parameters_noteNoWhy `parameters` is `None`, or what was masked or cut; `None` when complete.
service_clientsYesServices the node calls.
service_serversYesServices the node offers, its own parameter services included.
use_sim_time_noteNoWhy `use_sim_time` is `None`. `None` when it is set.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare read-only/idempotent/non-destructive, yet the description adds substantial behavior beyond them: secret masking by keyword with <masked>, truncation with original_size, timeout semantics, lock contention producing a `busy` error, and a hard 45 s ceiling with the lock-wait tradeoff. It also clarifies that the only request sent is a parameter list/get.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the critical scope restriction before any detail, and each sentence carries real information (masking, timeouts, error conditions, duration). It is dense and long for a single paragraph, which costs it a point, but there is little waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Even though an output schema exists, the description explains the non-obvious return semantics callers must interpret: null parameters vs. error, parameters_note, duplicate_count/note for ambiguous names, and masking/truncation markers. Combined with the duration and error model, an agent has everything needed to call and interpret it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and both params are documented in the schema, so the baseline is 3. The description largely restates node-name format and the 1..20/default-8 timeout, though it does add the nuance that a call that waited on the lock has less time for the parameter read.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb+resource ('Describe one ROS 2 node') and enumerates exactly what is returned (pub/sub topics, services/actions, params, use_sim_time). It also names the sibling list_participants as the alternative for DDS-only setups, so an agent can distinguish it from siblings without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing rule ('ROS 2 graph only; on a DDS-only setup use list_participants') plus clear when-not conditions: raises an MCP error for off-graph nodes, malformed names, or missing ros2 CLI, and a non-answering node is a finding to be reported rather than retried as an error.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_topic_infoA
Read-onlyIdempotent

ROS 2 graph only; on a DDS-only setup use list_endpoints. Describe one ROS 2 topic (fully qualified, e.g. /cmd_vel) as a TopicInfo: counts and, in live mode, publisher_qos / subscription_qos (reliability, durability where transient_local marks a latched topic such as /tf_static, endpoint_count; a null side says why in its _note) and publisher_nodes / subscriber_nodes. Raises an MCP error for a malformed or unknown topic, or without a ros2 CLI. Read-only; no side effects. Duration: runs the ros2 CLI behind the ROS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another ros2 call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault
topicYesFully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI.

Output Schema

ParametersJSON Schema
NameRequiredDescription
nameYes
noteNoOne sentence of context, or `None`.
message_typeYes
publisher_qosNoQoS of the publishers. `None` when it was not read: see `publisher_qos_note`.
mode_effectiveYes`live` or `mock` (fixtures).
publisher_countYes
publisher_nodesNoPublisher node names (`ros2 topic info --verbose`); empty when unknown.
subscriber_countYes
subscriber_nodesNoSubscriber node names; empty when unknown.
subscription_qosNoQoS of the subscriptions. `None` when it was not read: see `subscription_qos_note`.
publisher_qos_noteNoWhy `publisher_qos` is `None`. `None` when it is set.
subscription_qos_noteNoWhy `subscription_qos` is `None`. `None` when it is set.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare read-only/idempotent/no-destruction, and the description goes well beyond them: it discloses error-raising behavior, serializes via a ROS lock (one call at a time), a 45 s ceiling, and an immediate 'busy' failure with retry guidance. Lock contention and latency behavior are exactly the traits annotations cannot express.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The critical scope constraint is front-loaded before any detail. Information density is high and every sentence carries content, though the deeply nested parentheticals (QoS fields, endpoint notes) make it slightly harder to scan than ideal.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a one-parameter read tool it covers scope, prerequisites, failure modes, latency/concurrency behavior, and even enumerates return fields despite an output schema existing. Nothing an agent needs to invoke it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and the single parameter already documents the fully-qualified `/`-prefixed format and segment rules. The description restates the same format in prose without adding syntax or semantics beyond the schema, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('Describe one ROS 2 topic') with an output type ('TopicInfo') and precisely names the sibling it is not for ('on a DDS-only setup use list_endpoints'). An agent can distinguish it from list_topics, list_endpoints, and get_node_info without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly scopes the tool to the 'ROS 2 graph only' and routes to the alternative ('list_endpoints') for DDS-only setups. It further specifies failure conditions (malformed/unknown topic, missing ros2 CLI) that tell the agent when the call is and isn't appropriate.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

health_checkA
Read-onlyIdempotent

Report TopicForge environment state as a HealthReport: mode, ros_backend, dds_backend, ros_tools_available, ros2_distro, rmw_implementation, dds_domain_id and observed_domain_note (only the domain joined at startup is observed), server version and sample cap; for a live DDS backend also observer_started_ns / now_ns (nothing earlier was watched) and the discovery tracker status tracker_* (errors, evictions or a stale pass mean gaps). live with ros_backend none is a DDS-only setup: use list_endpoints. With dds_backend none, dds_inactive_note says why. payload_decoding is disabled (DDS user-topic payloads are not decoded); dds_security is not_supported. Always succeeds: call it first when something looks wrong. Read-only; no side effects. Duration: never waits for a lock and never runs the ros2 CLI; it answers at once.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
modeYesMode actually serving: `mock` or `live` (`live` with `ros_backend` `none`: DDS tools only). May differ from `requested_mode` after a fallback.
noteNoOne sentence of context, or `None`.
now_nsNoServer wall-clock time (ns since epoch) when this report was built.
rmw_sourceNo`env`, `ros2_cli`, `distro_default` (not verified on the graph) or `none`.
dds_backendNoDDS backend serving: `mock`, `cyclone`, `fast`, or `none` (DDS module inactive).
ros2_distroNo`ROS_DISTRO` if set (disclosed by design under the local-trust model).
ros_backendNoROS 2 backend: `ros2_cli`, `mock` or `none`.
dds_securityNoDDS Security is not handled: protected endpoints and data are invisible.
dds_domain_idNoDDS domain id observed when the DDS module is active.
requested_modeYesMode requested by configuration (may be `auto`).
ros2_availableYesWhether a `ros2` CLI is on PATH.
server_versionYesTopicForge version.
tracker_errorsNoTracker passes that raised; non-zero means gaps.
tracker_passesNoCompleted discovery tracker passes since start.
tracker_runningNoWhether the discovery tracker thread is alive (Cyclone); `None` without a tracker.
contract_versionNoOutput contract version; changes only when a field is renamed, removed or retyped.
max_sample_countYesServer cap on samples per call; larger requests are clamped.
payload_decodingNo`disabled`: DDS user-topic payloads are not decoded.
dds_inactive_noteNoWhy `dds_backend` is `none` while ROS 2 serves (not selected, binding missing, or failed to start); else `null`.
rmw_implementationNoROS 2 middleware, e.g. `rmw_fastrtps_cpp`; `None` when `rmw_source` is `none`.
observer_started_nsNons when the DDS observer joined; nothing earlier was watched. `None` without one.
ros_tools_availableNoTrue when the ROS 2 tools can run (`ros_backend` is not `none`); on a DDS-only setup use `list_endpoints`.
sim_clock_publishedNoWhether `/clock` had a publisher in the last graph read (a hint that stamps may be sim time). `None` in mock or without a recent read; `health_check` never runs `ros2`.
middleware_availableNoTrue when a DDS backend serves; else whether the configured binding is importable.
observed_domain_noteNoWhich DDS domain is observed: only the one joined at startup; others are invisible.
tracker_last_pass_nsNons of the last tracker pass; far older than `now_ns` means stale lifecycle.
payload_decoding_noteNoReason for `payload_decoding`.
tracker_cache_evictionsNoDiscovery entries dropped from a full tracker cache (4096 each); non-zero means the bus is bigger than listed.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already give readOnly/destructive/idempotent hints, yet the description adds substantial behavioral context: 'Always succeeds', 'never waits for a lock and never runs the ros2 CLI; it answers at once', and the fact that only the domain joined at startup is observed, so earlier activity is invisible. It also discloses capability limits: payload_decoding is disabled and dds_security is not supported.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is dense but every sentence carries load: field list, DDS-only routing, capability limits, success guarantee, and duration. It front-loads what the report contains and the 'call it first' guidance. Slightly long for a health check, but no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the output schema exists, the description still adds essential context: what the report fields mean, how to interpret tracker errors/evictions, why observed_domain_note is limited, and the two capability limitations. Nothing an agent needs to interpret or trust the result is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes no parameters, so there is nothing to document, and the description doesn't waste space on params. It instead explains what fields the response will contain, which is useful even though an output schema exists.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'Report TopicForge environment state as a HealthReport'. It immediately lists the report's fields, making the tool's output scope unmistakable. Distinguished from siblings like list_endpoints, which is named as the right tool for a DDS-only setup.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit 'call it first when something looks wrong', and names the alternative 'use list_endpoints' for a DDS-only setup. It also gives the condition under which the backend is DDS-only ('live' with ros_backend none).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_endpointsA
Read-onlyIdempotent

List every DDS endpoint (writer and reader) announced on the bus as an EndpointListing; one EndpointInfo each with role, dds_topic (raw, e.g. rt/scan), ros_topic (/scan; null with ros_topic_note for a non-ROS topic), type_name, type_id, the owning participant_guid with participant_name, and a structured qos. Use it instead of parsing peek_dds_samples and joining GUID prefixes. Large buses: pass fields compact to replace each qos by a one-line qos_summary and drop type_id, announced_ns and ros_topic_note; the default full is verbose, so scope with topic or participant_guid to read one topic's full QoS. Orphans: by_topic rolls endpoints up per topic with writer_count, reader_count, orphan (no_reader = a writer nobody subscribes to, no_writer = a reader nobody publishes to) and the union of partitions. In qos, a duration or policy of None is infinite/unset or not announced. Among EXCLUSIVE writers the live one with the highest ownership_strength delivers; which one owns an instance is runtime state TopicForge cannot observe. Departed endpoints: a leaving participant's endpoints are remembered (last 200, 1 h) and shown in by_topic as departed_writers / departed_readers (listed in endpoints only with include_departed), so a topic that lost its only writer is explained in one call. Topic filter: rt/scan and scan match each other; a filter matching nothing returns a note with the closest topics. This lists discovery facts, not data flow: it cannot tell a silent or hung writer from a healthy one; pair it with detect_qos_mismatches. The observer's own endpoints are excluded unless include_observer; ROS 2 service and action endpoints (rq/, rr/, rs/, rp/, ra/, ros_discovery_info) unless include_internal. Capped at 500 endpoints (truncated, total); by_topic covers all matches. Read-only. Raises an MCP error when no DDS module is active; mock returns a fixture. Duration: uses the DDS binding behind the DDS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another DDS call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault
topicNoOnly endpoints on this DDS topic: `scan` or `rt/scan` (exact name first, then the alternate form). Omit for every topic.
fieldsNo`full` (default) or `compact`: one-line `qos_summary` instead of the full `qos`, no `type_id`, `announced_ns` or `ros_topic_note`. Use `compact` on large buses.full
domain_idNoIgnored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` flags a different non-zero value.
include_departedNoAlso list endpoints whose participant left (flagged with `gone_ns`); `by_topic` reports them either way. Default false.
include_internalNoAlso list ROS 2 service and action endpoints (`rq/`, `rr/`, `rs/`, `rp/`, `ra/`, `ros_discovery_info`), most of a small robot's endpoints. Default false.
include_observerNoInclude TopicForge's own observer endpoints. Default false.
participant_guidNoOnly endpoints of this participant (a guid from `list_participants`). Omit for all.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoSet when a `topic` filter matched nothing: the closest known topics.
hintsNoFacts true of every endpoint, stated once (e.g. liveness is not observed).
totalYesEndpoints in the discovery cache before any filter.
fieldsNoDetail level served: `compact` drops per-endpoint QoS detail, type id and notes.
by_topicYesRoll-up over every matching endpoint.
returnedYes
domain_idYes
endpointsYesMatching endpoints, capped (see `truncated`).
truncatedYesTrue when matching endpoints exceeded the cap.
snapshot_nsYesWall-clock time of the snapshot, ns since epoch.
observer_guidNoGUID of TopicForge's own participant, `None` in mock.
mode_effectiveYes`live` or `mock` (fixtures).
departed_endpoint_countNoDeparted endpoints matching the filters; listed in `endpoints` only with `include_departed`.
hidden_internal_endpoint_countNoService/action endpoints (`rq/`, `rr/`, `rs/`, `rp/`, `ra/`, `ros_discovery_info`) left out while `include_internal` is false.
excluded_observer_endpoint_countNoTopicForge's own endpoints left out of `endpoints` (counted in `total`).

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover read-only/idempotent/open-world safety, but the description adds substantial behavior: an MCP error when no DDS module is active, mock fixture fallback, a 45s duration bound under the DDS lock with an immediate `busy` failure, a 500-endpoint cap with `truncated`/`total`, departed-endpoint retention (last 200, 1h), and exclusive-writer ownership semantics.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded purpose followed by clearly delimited bold-labelled sections (**Large buses**, **Orphans**, **Topic filter**, etc.) that make it scannable despite its length. It is dense and occasionally restates schema enum values, so it is slightly over-sized for a list tool, but nearly every sentence carries behavioral payload.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists so return shape needn't be re-explained, and annotations carry the safety profile; the description still covers error modes, caps, lock contention, orphan/departed semantics, and default exclusions. Nothing an agent needs to call this correctly appears missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the schema already documents each parameter in detail, so the baseline is 3. The description nonetheless adds cross-parameter meaning: what `compact` drops relative to `full`, that `by_topic` reports departed endpoints regardless of `include_departed`, that `domain_id` is only range-checked, and the `rt/scan` vs `scan` matching rule.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource ('List every DDS endpoint (writer and reader) announced on the bus as an EndpointListing') and enumerates the returned fields, so an agent knows exactly what it gets. It explicitly distinguishes itself from sibling `peek_dds_samples` and names `detect_qos_mismatches` as a complement.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use ('Use it instead of parsing peek_dds_samples and joining GUID prefixes'), when-to-scope (compact fields, topic/participant_guid on large buses), and when-not ('cannot tell a silent or hung writer from a healthy one; pair it with detect_qos_mismatches'). The alternative tool is named with the condition that selects it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_nodesA
Read-onlyIdempotent

ROS 2 graph only; on a DDS-only setup use list_participants. List the nodes on the ROS 2 graph (or the mock graph) from one ros2 node list call as a NodeListing: name, namespace, full_name, duplicate_count. duplicates lists full names used by several nodes (a per-node tool then answers for one of them, unidentified). Nodes starting with an underscore are hidden and a node on another ROS_DOMAIN_ID is invisible. Empty nodes when the graph has none. Use get_node_info for one node's interfaces. Raises an MCP error without a ros2 CLI. Read-only; cheap (one CLI call). Duration: runs the ros2 CLI behind the ROS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another ros2 call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoOne sentence of context, or `None`.
nodesYesNodes on the graph, one entry per distinct full name, sorted by name.
totalYesDistinct node names on the graph (no cap applies).
returnedYesLength of `nodes`.
truncatedYesTrue when `returned` is less than `total` because of a cap.
duplicatesYesFull names that appear more than once on the graph. Empty when every name is unique.
mode_effectiveYes`live` or `mock` (fixtures).

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnly, idempotent, non-destructive, openWorld), and the description adds genuinely new behavior: empty `nodes` on an empty graph, an MCP error without the `ros2` CLI, hidden underscore-prefixed nodes, cross-ROS_DOMAIN_ID invisibility, and the lock semantics (45 s ceiling, immediate `busy` error to retry). These are runtime traits the annotations cannot express.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the disambiguating scope clause before any detail, and each sentence carries distinct information (routing, return shape, edge cases, error/duration behavior). It is dense and bold-heavy, but nothing reads as filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present the description need not detail return values, yet it still flags the key semantic of `duplicates` and the empty-graph case. Combined with explicit failure modes (missing CLI, lock contention) and the sibling routing hint, an agent has everything needed to call this correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so the baseline is 4; there is nothing for the description to clarify. Its explanation of return fields (`duplicates`, `full_name`) goes beyond parameters, which is bonus context rather than parameter semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List the nodes on the ROS 2 graph') plus the data source ('one `ros2 node list` call') and the returned shape (`NodeListing` with name, namespace, full_name, duplicate_count). The opening clause immediately distinguishes it from `list_participants` for DDS-only setups, so an agent can route between the two without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use routing: 'ROS 2 graph only; on a DDS-only setup use `list_participants`' and 'Use `get_node_info` for one node's interfaces'. It also names the conditions that change behavior (mock graph, no ros2 CLI, busy lock) so the agent knows when this call will succeed or fail.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_participantsA
Read-onlyIdempotent

List DDS participants observed on the bus as a ParticipantListing: guid, vendor (read from the GUID prefix, unknown for vendors that do not put theirs there, e.g. Dust DDS and RTI Connext; see vendor_source), optional name (the announced EntityName, e.g. lidar_driver; a participant with none is normal, not a fault: identify it by its topics), hostname, domain_id and is_observer (TopicForge's own read-only participant, listed like any other). Lifecycle: status (active / left), first_seen_ns / last_seen_ns (TopicForge's clock), announced_ns (DDS source timestamp) and, once left, lost_ns + lost_time_source: an upper bound of the death (exact after a clean shutdown; after a crash the lease expiry, so up to one lease earlier: 10 s Cyclone, 20 s Fast DDS, 100 s RTI). Restarts: a restarted node is a new GUID with the same name; restarts_by_name gives lives (GUIDs) and departures per name, so do not count GUIDs by hand (seen_count is not a restart count). Cyclone tracks discovery in the background; right after start the call waits up to 3 s for it. Only the domain joined at startup is observed (health_check dds_domain_id): a participant on another domain is INVISIBLE here, and domain_id does not switch domains (restart with TOPICFORGE_DDS_DOMAIN_ID). Works at the raw DDS layer, so non-ROS participants appear too. Read-only by architecture: it cannot publish, modify QoS or alter the bus. Raises an MCP error when no DDS module is active (install topicforge[dds], set TOPICFORGE_DDS_BACKEND=cyclone); the mock backend returns fixtures. Duration: uses the DDS binding behind the DDS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another DDS call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault
domain_idNoIgnored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` flags a different non-zero value.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoOne sentence of context, or `None`.
totalYes
returnedYes
domain_idYesDDS domain observed (joined at startup).
truncatedYes
participantsYes
restarts_noteNoSet when unnamed participants were left out of `restarts_by_name`.
mode_effectiveYes`live` or `mock` (fixtures).
restarts_by_nameNoNames seen with several GUIDs or lost at least once (absent: ran once and stayed). A restart is a new GUID with the same name.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes far beyond the annotations by disclosing lock contention (DDS lock, one call at a time, 45 s bound, immediate 'busy' error with retry), lease-expiry error bounds per vendor, restart/GUID semantics, and the MCP error raised when no DDS module is active. It also confirms read-only 'by architecture', reinforcing rather than contradicting the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose and return shape are front-loaded, and nearly every sentence carries non-obvious operational detail (restart counting, timing sources, domain limits). It is dense and heavily parenthetical, verging on overload, but the size is largely justified by the tool's subtle semantics.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only discovery tool with an output schema, it supplies everything an agent needs: semantics of each field, timing caveats, failure modes, and domain constraints. Nothing material is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the single parameter is fully described in-schema, but the description still adds real meaning: domain_id is ignored, cannot switch domains, and the only way to change domain is restarting with TOPICFORGE_DDS_DOMAIN_ID. That goes beyond the field's range-check description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List DDS participants observed on the bus') and immediately names the return type (ParticipantListing) with its fields. The raw-DDS-layer and 'observed on the bus' scoping distinguishes it from the ROS-oriented siblings like list_nodes and from the event-oriented participant_events.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear operating context: only the startup domain is observed, a non-DDS participant is normal not a fault, and GUIDs should not be counted as restarts (use restarts_by_name). It does not, however, explicitly route the agent between this tool and alternatives such as participant_events or list_endpoints, so it stops short of full alternative selection guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_topicsA
Read-onlyIdempotent

ROS 2 graph only; on a DDS-only setup use list_endpoints. List the topics on the ROS 2 graph (or the mock graph) as a TopicListing: each has name, message_type, publisher_count and subscriber_count; mode_effective tells a real graph from fixtures. QoS is not listed: use get_topic_info. Empty topics when the graph has none or live discovery times out. Raises an MCP error without a ros2 CLI. Read-only; no side effects. Duration: runs the ros2 CLI behind the ROS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another ros2 call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoOne sentence of context, or `None`.
totalYes
topicsYes
returnedYes
truncatedYes
mode_effectiveYes`live` or `mock` (fixtures).

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well past the annotations (readOnly/idempotent/openWorld) by disclosing failure modes: empty `topics` on no topics or discovery timeout, an MCP error when no `ros2` CLI exists, serialization behind a ROS lock, a 45 s ceiling including lock wait, and a fast `busy` error that should be retried. That is exactly the operational context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the scope and the alternative, and every sentence carries actionable content. It is dense and slightly over-packed with bolded caveats, but no sentence is filler, so it stops short of a 5 only on sheer density.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite an output schema existing, the description still supplies the discriminating context an agent needs: the mock-vs-real signal, the empty-result semantics, the CLI prerequisite, and the locking/retry protocol. Nothing required to select or invoke it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so the baseline of 4 applies. The description adds meaning to the *result* fields (`mode_effective`, counts) rather than parameters, which is beyond the schema's scope but correct for the dimension.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource — 'List the topics on the ROS 2 graph' — and immediately scopes it against the DDS-only case by naming `list_endpoints`. It even enumerates the returned fields, so an agent knows exactly what it gets and how it differs from `list_nodes` or `get_topic_info`.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing: ROS 2 graph only, use `list_endpoints` on DDS-only setups, and use `get_topic_info` because QoS is deliberately not included here. Both the when-to-use and the when-not-to-use are stated with the named alternative.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

participant_eventsA
Read-onlyIdempotent

Return DDS participant lifecycle events (discovered / lost) from a recent window, e.g. 'who left in the last 5 minutes?', as a ParticipantEventListing, newest first: guid, event_type, vendor, timestamp_ns, time_source, observed_ns, age_s (seconds before the response; live only), optional name (announced name), hostname and domain_id. time_source is dds_source_timestamp (timestamp of the announcement or dispose) or observed_local (when TopicForge noticed, weakest). Crash caveat: a lost timestamp is an upper bound of the death: exact after a clean shutdown, the lease expiry after a crash, so the process died up to one lease earlier (10 s Cyclone, 20 s Fast DDS, 100 s RTI); the two cases cannot be told apart. Restarts: a restarted node is a new participant (one lost and one discovered, different guid, same name); restarts_by_name gives lives and departures per name over the window, so do not count GUIDs by hand. Capped at 200 events (truncated, note; reduce lookback_s). TopicForge only knows what happened since it started watching (health_check.observer_started_ns). Backends: Fast DDS uses listener callbacks; Cyclone polls discovery in the background every 0.5 s, so a participant cycle faster than the reader's history depth between two passes can be missed; mock returns a fixture timeline. Right after start the call waits up to 3 s for discovery. Read-only by architecture. Raises an MCP error when no DDS module is active. Duration: uses the DDS binding behind the DDS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another DDS call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault
domain_idNoIgnored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` flags a different non-zero value.
lookback_sNoWindow in seconds, 1..86400, default 300. A larger window may hit the 200-event cap.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoOne sentence of context, or `None`.
totalYes
eventsYes
returnedYes
domain_idYesDDS domain observed (joined at startup).
truncatedYesTrue when cut to the 200-event cap.
restarts_noteNoSet when unnamed participants were left out of `restarts_by_name`.
mode_effectiveYes`live` or `mock` (fixtures).
restarts_by_nameNoNames seen with several GUIDs or lost at least once (absent: ran once and stayed). A restart is a new GUID with the same name. Window only.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes far beyond the annotations, which already declare read-only/idempotent: it discloses the crash lease-expiry ambiguity (10/20/100 s), restart semantics, the 200-event cap with `truncated`/`note`, backend-specific polling gaps, a 3 s startup wait, an MCP error when no DDS module is active, and 45 s lock behavior with a `busy` retry. 'Read-only by architecture' is consistent with readOnlyHint=true.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core purpose and every later sentence (crash caveat, restarts, cap, backends, lock) carries operational weight. It is dense and somewhat long, with minor redundancy (enumerating output fields that the output schema already provides, restating read-only), keeping it from a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with an output schema, a 100%-covered input schema, and four annotations, the description covers the remaining risk surface completely: error conditions, lock contention, capping, backend variance, and time-source caveats. An agent can call it correctly without further inquiry.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% so the baseline is 3; the description adds marginal leverage by tying the 200-event cap to reducing `lookback_s` and reiterating that `domain_id` does not switch domains. Most semantics are still carried by the schema, but the description reinforces the operative constraint.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Return DDS participant lifecycle events (`discovered` / `lost`) from a recent window') with an example query that pins the use case. The lifecycle-event framing (vs. current-state listing) intrinsically separates it from siblings like list_participants.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a concrete scenario ('who left in the last 5 minutes?') and steers the agent to `restarts_by_name` rather than hand-counting GUIDs, which is a real alternative-routing hint. However it never explicitly states when to prefer this over sibling tools like list_participants or health_check.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

peek_bag_samplesA
Read-onlyIdempotent

Peek up to count samples from a recorded bag file as a SampleResult: offline, for post-mortem (peek_dds_samples and sample_messages are live). Formats, by extension: MCAP (.mcap), rosbag2 SQLite (.db3), ROS 1 (.bag). Returns the first count messages of the topic in recording order, not the last. Clocks: timestamp_ns is header.stamp when the message has a top-level header (stamp_source header), else the time in the body of Clock, TFMessage or Log (payload), else the bag record time (recorded); recorded_ns is always the record time. Each payload carries _decode_status (full / partial / raw). count defaults to 5, clamped to 50. Uses the rosbags library and needs the ROS 2 side of the runtime (an error on a DDS-only setup). Mock returns fixtures on canned paths. A bag without embedded definitions (Humble .db3) is decoded with those of its recorded distro, or Humble; note says which. Arrays over 4096 elements are cut and note lists them. Read-only by architecture. Raises an MCP error when the path does not exist, the topic is not in the bag, or rosbags is missing. Summary and rate: LaserScan, Odometry, Imu, Image and PointCloud2 samples carry a summary of the whole message (other types: null); rate gives frequency, interval spread and a verdict measured on recorded_ns (the bag record time of the returned messages). Use count >= 10 (50 above 20 Hz); a multi-publisher topic such as /tf reads erratic without being broken. Duration: takes no lock (in-process file read, so it runs while a ROS 2 or DDS call is busy); the time follows the bag's size and how deep the first samples are.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYesBag path: a `.mcap` or `.db3` file or a `rosbag2_*` directory (`peek_bag_samples` also reads ROS 1 `.bag`; `analyze_bag` does not). Existence and format are checked live; mock accepts any well-formed path.
countNoMessages to return, default 5, at most 50 (`health_check.max_sample_count`): more is capped to 50, negative raises an error. The result's `count` may be lower.
topicYesFully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoWhy `samples` is empty or limited, when not obvious; else `None`.
rateNoHow the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10.
countYesNumber of samples in `samples`: fewer than requested when the topic gave fewer, never more than 50 (a larger request is capped to 50).
topicYes
samplesYesThe samples, in backend order.
mode_effectiveYes`live` or `mock` (fixtures).

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnly, idempotent, non-destructive), yet the description goes far beyond them: clock/timestamp resolution rules, `_decode_status` values, count clamping, `rosbags` dependency and runtime requirement, Humble `.db3` fallback decoding, 4096-element truncation with `note`, mock behavior, MCP-error conditions, and lock-free duration characteristics. This is unusually rich disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The purpose and the sibling distinction are front-loaded, and nearly every sentence carries operational detail. It is dense and heavily bolded, and some return-value semantics (summary/rate fields) lean on material the output schema could carry, but there is little actual filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 3-parameter tool with an output schema, the description covers dependencies, supported formats, error conditions, edge cases (Humble decoding, array truncation) and timing, leaving nothing an agent needs to invoke it correctly or anticipate failure. Return-value explanation is largely redundant with the output schema but harmless.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds meaning the schema lacks: the `count` >= 10 recommendation for meaningful `rate`, and the note that a multi-publisher topic like `/tf` reads `erratic` without being broken. This contextualizes the parameters rather than restating them.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb+resource ('Peek up to `count` samples from a recorded bag file') and immediately scopes it ('offline, for post-mortem'), then explicitly distinguishes the two live siblings (`peek_dds_samples`, `sample_messages`). An agent can identify the tool and its unique niche without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly names the alternatives and the condition that selects them ('peek_dds_samples and sample_messages are live'), and the schema further notes `analyze_bag` cannot read ROS 1 `.bag`. It also gives prescriptive usage advice ('Use `count` >= 10 (50 above 20 Hz)') and the environment preconditions (needs the ROS 2 side of the runtime).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

peek_dds_samplesA
Read-onlyIdempotent

Peek recent samples on a raw DDS topic as a SampleResult, like sample_messages but read from the DDS layer (works without ROS 2). count defaults to 5, clamped to 50. (a) The builtin discovery topics (DCPSParticipant, DCPSSubscription, DCPSPublication) return structured discovery records: on Cyclone the CURRENT state (one per live participant or endpoint), not recent events (use participant_events); DCPSPublication / DCPSSubscription are the raw data behind list_endpoints. The topic may be /scan, scan or rt/scan. (b) User topics: payload decoding is DISABLED on every backend, so the call returns count 0, empty samples and a note; that does NOT mean the topic is silent. Use list_endpoints for its presence, writers, readers and QoS. A user topic not announced on the bus raises an error. Right after start the call waits up to 3 s for discovery. Read-only by architecture: it cannot publish. Raises an MCP error when no DDS module is active or the topic is not announced. Duration: uses the DDS binding behind the DDS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another DDS call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault
countNoMessages to return, default 5, at most 50 (`health_check.max_sample_count`): more is capped to 50, negative raises an error. The result's `count` may be lower.
topicYesDDS topic name: bare (`scan`), ROS 2 mangled (`rt/scan`) or a builtin discovery topic (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Letters, digits, `_`, `/` and `::` only.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoWhy `samples` is empty or limited, when not obvious; else `None`.
rateNoHow the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10.
countYesNumber of samples in `samples`: fewer than requested when the topic gave fewer, never more than 50 (a larger request is capped to 50).
topicYes
samplesYesThe samples, in backend order.
mode_effectiveYes`live` or `mock` (fixtures).

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

This description discloses rich behavioral context that goes far beyond the annotations. It explains the error conditions (no DDS module active, topic not announced), the locking/duration behavior (45 sec max, possible 'busy' error), the 3-second discovery wait, and the fact that user topics return empty samples with a note. All these traits are not derivable from the annotations or schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but well-structured and front-loaded. It uses bold emphasis and parenthetical clauses to separate key concepts (builtin topics, user topics, error conditions). While it is relatively long, every sentence provides crucial information without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of the tool (interacting with DDS, multiple topic types, locking, error modes) and the presence of both annotations and an output schema, the description is extremely complete. It covers all edge cases an agent needs to know to call the tool correctly and interpret results, leaving no significant gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description adds meaning by explaining that 'count' defaults to 5, is clamped to 50, and that negatives raise an error, and that the result's 'count' may be lower. It also clarifies the acceptable topic name formats (bare, ROS 2 mangled, builtin discovery topics). This extra information enhances the agent's ability to use the parameters correctly beyond what the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource ('Peek recent samples on a raw DDS topic') and explicitly differentiates itself from the closest sibling, 'sample_messages', by noting that it reads from the DDS layer (works without ROS 2). It further clarifies the tool's scope by distinguishing builtin discovery topics from user topics.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit when-to-use and when-not-to-use guidance. It directs the agent to use 'participant_events' for recent events on builtin topics and 'list_endpoints' for checking user topic presence and QoS. It also clearly warns that user topics are not decodable, preventing a common misuse (thinking a topic is silent when it is just undecodable).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

sample_messagesA
Read-onlyIdempotent

ROS 2 graph only; on a DDS-only setup use list_endpoints or peek_dds_samples on DCPSPublication / DCPSSubscription. Collect up to count live messages from a ROS 2 topic, waiting at most timeout_s seconds, as a SampleResult. Live mode streams ros2 topic echo with the publishers' QoS (latched topics work) and returns what arrived by the deadline: count is the actual number and note says N of M messages and why it is short (no publisher, silent, slow or too large to print in time; a latched topic usually holds one message, so use count 1). Each sample has payload (nested named values), timestamp_ns (the publisher's clock, sim time on a simulation, 0 for a headerless message such as std_msgs/String; see stamp_source) and received_ns (wall clock when the CLI printed it). Arrays: arrays, strings and bytes are cut at 128 elements by default (a 541-beam LaserScan loses beams 128 and up); cut fields are listed in payload._truncated_fields and note. Raise max_array_length (up to 65536, or null) or set arrays_summary_only. A message over the 1 MiB cap (TOPICFORGE_MAX_SAMPLE_BYTES) is dropped with a note. Mock mode returns demo-robot samples instantly. Raises an MCP error without a ros2 CLI, for an unknown topic, or when the CLI fails. Read-only; never publishes. Summary and rate: LaserScan, Odometry, Imu, Image and PointCloud2 samples carry a summary of the whole message (other types: null); rate gives frequency, interval spread and a verdict measured on received_ns (when the CLI printed each message). Use count >= 10 (50 above 20 Hz); a multi-publisher topic such as /tf reads erratic without being broken. Duration: runs the ros2 CLI behind the ROS lock, one call at a time. It returns within timeout_s (at most 40) plus about 3 s to stop the CLI and decode; the whole call, lock wait included, ends within 45 s: a call that cannot get the lock fails at once with a busy error (retry), and one that waited has less time to collect.

ParametersJSON Schema
NameRequiredDescriptionDefault
countNoMessages to wait for, default 5, at most 50 (more is capped and `note` says so); 0 returns nothing. Fewer arrive when the topic is slow or `timeout_s` runs out.
topicYesFully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI.
timeout_sNoSeconds to wait, 1..40, default 10, covering topic lookup and CLI start-up. The call returns within `timeout_s` plus a few seconds, with what arrived and a `note`. Raise it for a topic slower than 1 Hz.
max_array_lengthNoLongest array, string or bytes value returned in full, 1..65536, default 128 (a 541-beam `LaserScan` is cut to 128 ranges; its `summary` still covers all). Longer values are cut and listed in `_truncated_fields`. Null returns everything (large for images and clouds; a message over 1 MiB is dropped with a note).
arrays_summary_onlyNoWhen true, arrays become a type-and-length summary: use it to read the non-array fields of large messages. Default false.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoWhy `samples` is empty or limited, when not obvious; else `None`.
rateNoHow the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10.
countYesNumber of samples in `samples`: fewer than requested when the topic gave fewer, never more than 50 (a larger request is capped to 50).
topicYes
samplesYesThe samples, in backend order.
mode_effectiveYes`live` or `mock` (fixtures).

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover the safety profile (readOnly, idempotent, non-destructive), but the description adds substantial behavior beyond them: MCP error conditions, the 128-element array cut and 1 MiB drop cap, lock serialization with a `busy` retry error, and the 45 s hard deadline. These are exactly the traits an agent needs and none are in the structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The routing constraint and core purpose are front-loaded, and bold section markers (Live mode, Arrays, Duration) aid scanning. It is dense and long, with some redundancy between the prose and schema (e.g. the `_truncated_fields` explanation appears in both), but nearly every sentence carries operational weight for a complex tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a five-parameter tool with dual modes, lock contention, and multiple failure paths, the description covers timing, error conditions, truncation, and rate measurement. An output schema exists, yet the extra detail on `SampleResult` fields and `note` values makes failure diagnosis possible without the caller guessing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real semantics: what `count` vs `note` mean, how `max_array_length` interacts with `_truncated_fields` and the byte cap, and what `arrays_summary_only` is for. It goes beyond the schema descriptions rather than repeating them.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Collect up to `count` live messages from a ROS 2 `topic`') and immediately scopes it against alternatives by naming `list_endpoints` and `peek_dds_samples` for DDS-only setups. An agent can distinguish this from siblings without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says when to use this tool (ROS 2 graph) versus when not (DDS-only setups, routed to specific siblings), separates live versus mock mode, and gives operational rules like `count` >= 10 for rate and raising `timeout_s` for sub-1 Hz topics. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

topic_metricsA
Read-onlyIdempotent

Return temporal metrics (frequency, sequence gaps, latency percentiles) for a DDS topic over a recent window as a TopicMetrics. Read status first: unsupported_user_topic means the payload is not decoded, so no metric exists: every null or 0 is not a measurement; no_samples_yet is a builtin topic with nothing buffered; ok means computed. Limits: the buffer is filled only when peek_dds_samples runs on the topic, so observed_frequency_hz reflects how often it was called, not the publish rate: a coarse presence signal. declared_frequency_hz is 1 / deadline of the shortest writer Deadline announced in discovery (declared, not measured; null when none). Also sequence_gaps_count, latency_ns_p50/p95/p99 and availability flags. Read-only by architecture. Raises an MCP error when no DDS module is active or window_s is outside 1..3600. Right after start the call waits up to 3 s for discovery. Duration: uses the DDS binding behind the DDS lock (one call at a time); the whole call, lock wait included, ends within 45 s, and if another DDS call holds the lock it fails at once with a busy error (retry).

ParametersJSON Schema
NameRequiredDescriptionDefault
topicYesDDS topic name: bare (`scan`), ROS 2 mangled (`rt/scan`) or a builtin discovery topic (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Letters, digits, `_`, `/` and `::` only.
window_sNoWindow in seconds, 1..3600, default 60.
domain_idNoIgnored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` flags a different non-zero value.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNoOne sentence of context, or `None`.
topicYes
statusNo`ok`; `no_samples_yet` (empty buffer); `unsupported_user_topic` (payload not decoded: null fields are not measurements).
window_sYesRequested window in seconds (echoed).
latency_ns_p50NoMedian publish-to-receive latency in ns; `None` unless `latency_available`.
latency_ns_p95No95th-percentile publish-to-receive latency (ns).
latency_ns_p99No99th-percentile publish-to-receive latency (ns).
mode_effectiveYes`live` or `mock` (fixtures).
window_actual_sYesSeconds actually covered; shorter than `window_s` after a recent start; `0.0` without samples.
samples_observedYesSamples buffered in the window. `0` does NOT mean no publisher, only that no `peek_dds_samples` call captured one.
latency_availableNoTrue when a sample carried both a publish and a receive timestamp.
sequence_gaps_countNoMissing sequence numbers in the buffered samples; `0` also when none exist (see `sequence_numbers_available`).
declared_frequency_hzNo`1 / deadline` of the shortest writer Deadline announced: the promised rate, not the observed one; `None` when none announced.
observed_frequency_hzNo`samples_observed / window_actual_s`; `None` under 2 samples.
sequence_numbers_availableNoTrue when a sequence number could be read from a sample.

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare read-only/idempotent/non-destructive, and the description goes well beyond them: failure modes (MCP error when no DDS module is active or window_s is out of 1..3600), a 3 s discovery wait right after startup, a 45 s overall bound under the DDS lock, and immediate `busy` errors with a retry instruction when another DDS call holds the lock. Semantics of observed vs declared frequency and the caveat that null/0 under non-`ok` statuses are not measurements are disclosed too.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense but deliberately structured with bolded section labels (status, Limits, Duration) and the core purpose front-loaded in the first sentence. It is longer than most descriptions and repeats a couple of schema-stated constraints, but nearly every sentence carries operational information an agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers everything an agent needs to call it correctly despite the complexity: status gating, measurement vs declared semantics, error and lock-timing behavior, and read-only architecture. Since an output schema exists, the description is not obligated to detail return fields and wisely spends its words on semantics and failure modes instead.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the topic format, window_s range/default, and the fact that domain_id is ignored (range-checked only) are already documented in the schema. The description largely restates those constraints rather than adding syntax, format, or default information beyond them, making the baseline 3 appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Return temporal metrics ... for a DDS topic over a recent window as a TopicMetrics') and enumerates what those metrics are (frequency, sequence gaps, latency percentiles). This clearly differentiates it from siblings like get_topic_info (metadata) and peek_dds_samples (raw samples), which the description also names as the buffer producer.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives actionable usage guidance: 'Read `status` first' with an interpretation of each status value, and explains the dependency on peek_dds_samples having run ('the buffer is filled only when peek_dds_samples runs'). It clarifies the coarse nature of observed_frequency_hz so the agent knows when not to trust it, though it never states a scenario in which a sibling should be preferred instead.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 14 tool updatesv0.7.1
    • Changedanalyze_bag42 fields changed
      • changedInput schema / properties / path / description
        Previous value: -"Path to a bag: a file ending in `.mcap` or `.db3`, or a `rosbag2_*` directory. `peek_bag_samples` also reads ROS 1 `.bag` files; `analyze_bag` does not (`ros2 bag info` cannot open them). Leading/trailing whitespace is stripped. Null bytes and otherwise malformed filesystem paths are rejected. Existence and bag format are validated by the live adapter (mock mode accepts any well-formed path)."New value: +"Bag path: a `.mcap` or `.db3` file or a `rosbag2_*` directory (`peek_bag_samples` also reads ROS 1 `.bag`; `analyze_bag` does not). Existence and format are checked live; mock accepts any well-formed path."
      • removedInput schema / properties / path / title
        Removed value: -"Path"
      • removedInput schema / title
        Removed value: -"analyze_bagArguments"
      • removedOutput schema / $defs / BagTopicStats / description
        Removed value: -"Per-topic statistics inside a bag analysis result."
      • changedOutput schema / $defs / BagTopicStats / properties / first_timestamp_ns / description
        Previous value: -"Recorded time of the first message on this topic, in nanoseconds (bag receive time, normally epoch). `null` when the bag was summarized from `ros2 bag info` text only, or the topic has no messages."New value: +"Recorded ns of the first message; `null` for text-only summaries or no messages."
      • removedOutput schema / $defs / BagTopicStats / properties / first_timestamp_ns / title
        Removed value: -"First Timestamp Ns"
      • changedOutput schema / $defs / BagTopicStats / properties / frequency_basis / description
        Previous value: -"How `frequency_hz` was computed: `topic_span` (per-topic first and last message times, the accurate one) or `bag_duration` (count divided by whole-bag duration, used when per-topic times could not be read). `null` when there is no rate."New value: +"`topic_span` (accurate) or `bag_duration` (count over the whole bag, understates late topics); `null` without a rate."
      • removedOutput schema / $defs / BagTopicStats / properties / frequency_basis / title
        Removed value: -"Frequency Basis"
      • changedOutput schema / $defs / BagTopicStats / properties / frequency_hz / description
        Previous value: -"Average publish rate in Hz. With `frequency_basis` `topic_span` it is `(message_count - 1) / (last - first message time)` for this topic; with `bag_duration` it is `message_count / bag duration`, which understates the rate of a topic that started late or stopped early. `null` when fewer than 2 messages, a zero time span, or the topic is `latched` and all its messages fall within 1 second (a start-up burst, not a rate). A latched topic published over a longer span keeps its rate. Not a periodic rate for event-driven topics such as `/parameter_events`."New value: +"Average Hz: (count - 1) over the topic's own time span (see `frequency_basis`); `null` under 2 messages, a zero span, or a latched start-up burst (all within 1 s)."
      • removedOutput schema / $defs / BagTopicStats / properties / frequency_hz / title
        Removed value: -"Frequency Hz"
      • addedOutput schema / $defs / BagTopicStats / properties / kind
        Added value: +{
        +  "default": "user",
        +  "description": "`user` (data, incl. `/tf` and `/clock`), `rosbag2_internal` or `ros_builtin` (e.g. `/rosout`).",
        +  "enum": [
        +    "user",
        +    "rosbag2_internal",
        +    "ros_builtin"
        +  ],
        +  "type": "string"
        +}
      • changedOutput schema / $defs / BagTopicStats / properties / last_timestamp_ns / description
        Previous value: -"Recorded time of the last message on this topic; same rules as `first_timestamp_ns`."New value: +"Recorded ns of the last message; same rules."
      • removedOutput schema / $defs / BagTopicStats / properties / last_timestamp_ns / title
        Removed value: -"Last Timestamp Ns"
      • changedOutput schema / $defs / BagTopicStats / properties / latched / description
        Previous value: -"True when a publisher recorded this topic with transient_local durability (late joiners get the last samples, e.g. `/tf_static`, `/rosout`). Such a topic often publishes a start-up burst; when all its messages fall within 1 second `frequency_hz` is `null`. `false` when every recorded publisher was volatile; `null` when the bag carries no QoS information (ROS 1 bags report their latching flag)."New value: +"True if recorded with transient_local durability, false if volatile, `null` without QoS information."
      • removedOutput schema / $defs / BagTopicStats / properties / latched / title
        Removed value: -"Latched"
      • removedOutput schema / $defs / BagTopicStats / properties / message_count / title
        Removed value: -"Message Count"
      • removedOutput schema / $defs / BagTopicStats / properties / message_type / description
        Removed value: -"ROS2 message type recorded for this topic."
      • removedOutput schema / $defs / BagTopicStats / properties / message_type / title
        Removed value: -"Message Type"
      • removedOutput schema / $defs / BagTopicStats / properties / name / description
        Removed value: -"Fully qualified topic name as recorded in the bag."
      • removedOutput schema / $defs / BagTopicStats / properties / name / title
        Removed value: -"Name"
      • removedOutput schema / $defs / BagTopicStats / title
        Removed value: -"BagTopicStats"
      • removedOutput schema / $defs / ParticipantInfo
        Removed value: -{
        -  "additionalProperties": false,
        -  "description": "DDS participant discovered on the configured domain.\n\nCarries identity (`guid`, `name`, `vendor`) plus lifecycle fields\n(`first_seen_ns`, `last_seen_ns`, `status`, `seen_count`).",
        -  "properties": {
        -    "announced_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Nanoseconds since epoch (DDS source timestamp of the participant's most recent announcement, taken from the announcing side's clock; the local clock when the announcer sent none). `None` when the backend does not expose it. Distinct from `first_seen_ns` / `last_seen_ns`, which are TopicForge's own local clock.",
        -      "title": "Announced Ns"
        -    },
        -    "domain_id": {
        -      "description": "DDS domain id the participant is bound to.",
        -      "maximum": 232,
        -      "minimum": 0,
        -      "title": "Domain Id",
        -      "type": "integer"
        -    },
        -    "first_seen_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Wall-clock timestamp (nanoseconds since epoch) of the **first** discovery sample TopicForge observed for this participant. `None` when the adapter does not track lifecycle.",
        -      "title": "First Seen Ns"
        -    },
        -    "guid": {
        -      "description": "DDS Global Unique Identifier of the participant: hex string, stable across discovery events within a single deployment.",
        -      "title": "Guid",
        -      "type": "string"
        -    },
        -    "hostname": {
        -      "anyOf": [
        -        {
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Hostname announced in discovery (Cyclone `__Hostname` property). `None` when the vendor does not announce it.",
        -      "title": "Hostname"
        -    },
        -    "is_observer": {
        -      "default": false,
        -      "description": "True for TopicForge's own read-only observer participant, which joins the domain to watch it and appears in this list; false for every other participant.",
        -      "title": "Is Observer",
        -      "type": "boolean"
        -    },
        -    "last_seen_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Wall-clock timestamp (nanoseconds since epoch) of the **most recent** discovery sample observed. Used by `participant_events` to distinguish currently-active participants from those that left the bus. `None` when the adapter does not track lifecycle.",
        -      "title": "Last Seen Ns"
        -    },
        -    "lost_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Nanoseconds since epoch of the moment the participant left, once `status` is `left`; `None` while it is active. How precise it is depends on `lost_time_source`. It is an upper bound of when the participant died: after a clean shutdown it is the exact leave time, after a crash it is when the lease expired, and the two cannot be told apart. The process died at or before `lost_ns`, and at most one lease earlier (10 s Cyclone default, 20 s Fast, 100 s RTI).",
        -      "title": "Lost Ns"
        -    },
        -    "lost_time_source": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "dds_source_timestamp",
        -            "observed_local"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Where `lost_ns` comes from: `dds_source_timestamp` (timestamp carried by the discovery dispose) or `observed_local` (the moment TopicForge noticed, the weakest). `None` while active.",
        -      "title": "Lost Time Source"
        -    },
        -    "mode_effective": {
        -      "description": "Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`.",
        -      "enum": [
        -        "mock",
        -        "live"
        -      ],
        -      "title": "Mode Effective",
        -      "type": "string"
        -    },
        -    "name": {
        -      "anyOf": [
        -        {
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Participant name the application announced through the standard DDS EntityName QoS (PID_ENTITY_NAME in RTPS discovery), for example `lidar_driver`. `None` when the application did not set one (Dust DDS cannot) or the backend does not expose it.",
        -      "title": "Name"
        -    },
        -    "seen_count": {
        -      "default": 1,
        -      "description": "Number of distinct discovery samples observed for this participant across all calls to `list_participants` during this server's lifetime. Starts at 1 and increments on each observation.",
        -      "minimum": 1,
        -      "title": "Seen Count",
        -      "type": "integer"
        -    },
        -    "status": {
        -      "default": "unknown",
        -      "description": "Lifecycle status. `active` means TopicForge has a recent discovery sample for this GUID and the bus has not signalled removal. `left` means the participant was observed earlier but has since disappeared (a Fast DDS `REMOVED` callback or a Cyclone polling delta). `unknown` when the adapter does not track lifecycle.",
        -      "enum": [
        -        "active",
        -        "left",
        -        "unknown"
        -      ],
        -      "title": "Status",
        -      "type": "string"
        -    },
        -    "vendor": {
        -      "description": "DDS implementation that announced this participant, decoded from the OMG-RTPS `vendor_id` field on the discovery sample. `cyclone` (Eclipse Cyclone DDS), `fast` (eProsima Fast DDS), `rti` (RTI Connext), `rti_micro` (RTI Connext Micro), `opensplice` (ADLink OpenSplice), `opendds` (OCI OpenDDS), `coredx` (Twin Oaks CoreDX), `intercom` (Kongsberg InterCOM), `dust` (S2E Dust DDS). `mock` is reserved for synthetic fixtures. `unknown` means the vendor could not be determined: TopicForge reads it from the vendor prefix of the participant GUID, and some vendors (for example Dust DDS and RTI Connext) do not put their vendor id there, while the Cyclone Python binding does not expose the RTPS header vendor id. The participant is still reported; see `vendor_source`. Any conformant DDS-RTPS participant should appear through standard discovery; Cyclone DDS and Dust DDS have been observed on a live bus. See `docs/dds-interop-matrix.md`.",
        -      "enum": [
        -        "cyclone",
        -        "fast",
        -        "rti",
        -        "rti_micro",
        -        "opensplice",
        -        "opendds",
        -        "coredx",
        -        "intercom",
        -        "dust",
        -        "mock",
        -        "unknown"
        -      ],
        -      "title": "Vendor",
        -      "type": "string"
        -    },
        -    "vendor_source": {
        -      "default": "none",
        -      "description": "Where `vendor` came from: `guid_prefix` when it was read from the vendor prefix of the participant GUID, `none` when the vendor is unknown (or synthetic in mock mode).",
        -      "enum": [
        -        "guid_prefix",
        -        "none"
        -      ],
        -      "title": "Vendor Source",
        -      "type": "string"
        -    }
        -  },
        -  "required": [
        -    "guid",
        -    "vendor",
        -    "domain_id",
        -    "mode_effective"
        -  ],
        -  "title": "ParticipantInfo",
        -  "type": "object"
        -}
      • changedOutput schema / description
        Previous value: -"Structured summary of a ROS2 bag.\n\nThe fields `bag_format`, `samples_decoded_count`,\n`recording_duration_ns` and `participants_recorded` are populated only\nwhen the `rosbags`-backed reader runs; the `ros2 bag info` path leaves\nthem at their defaults."New value: +"Structured summary of a ROS2 bag."
      • changedOutput schema / properties / anomalies / description
        Previous value: -"Human-readable notes about gaps, clock jumps, or other oddities. Populated in mock mode only; live mode does not detect anomalies."New value: +"Notes on gaps or clock jumps; mock mode only."
      • removedOutput schema / properties / anomalies / title
        Removed value: -"Anomalies"
      • changedOutput schema / properties / bag_format / description
        Previous value: -"Concrete bag container format detected by the reader: `mcap` (Foxglove MCAP), `db3` (ROS2 rosbag2 SQLite), `bag` (ROS1 legacy chunked), or `unknown` when the reader could not classify. `None` when the bag was summarized from `ros2 bag info` text, which carries no format information."New value: +"Detected container: `mcap`, `db3`, `bag` or `unknown`; `None` for `ros2 bag info` text."
      • removedOutput schema / properties / bag_format / title
        Removed value: -"Bag Format"
      • addedOutput schema / properties / duration_s
        Added value: +{
        +  "description": "Bag duration in seconds.",
        +  "minimum": 0,
        +  "type": "number"
        +}
      • removedOutput schema / properties / duration_seconds
        Removed value: -{
        -  "description": "Total bag duration, in seconds (wall clock between first and last message).",
        -  "minimum": 0,
        -  "title": "Duration Seconds",
        -  "type": "number"
        -}
      • removedOutput schema / properties / message_count / title
        Removed value: -"Message Count"
      • changedOutput schema / properties / mode_effective / description
        Previous value: -"Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`."New value: +"`live` or `mock` (fixtures)."
      • removedOutput schema / properties / mode_effective / title
        Removed value: -"Mode Effective"
      • changedOutput schema / properties / note / description
        Previous value: -"Why the result is less detailed than usual, for example per-topic rates computed over the whole bag duration because the bag was too large to read per-topic message times. `None` when there is nothing to add."New value: +"Why the result is less detailed than usual (e.g. rates over the whole bag duration); else `None`."
      • removedOutput schema / properties / note / title
        Removed value: -"Note"
      • removedOutput schema / properties / participants_recorded
        Removed value: -{
        -  "description": "DDS participants recorded in the bag when the container format embeds participant metadata. MCAP can carry it via channel metadata records; ROS2 `.db3` and ROS1 `.bag` generally do not. Empty list when not available, which is the common case.",
        -  "items": {
        -    "$ref": "#/$defs/ParticipantInfo"
        -  },
        -  "title": "Participants Recorded",
        -  "type": "array"
        -}
      • changedOutput schema / properties / path / description
        Previous value: -"Path to the analyzed bag, as supplied by the caller. May point to a file (`.mcap`, `.db3`, or ROS 1 `.bag` for `peek_bag_samples`) or to a `rosbag2_*` directory."New value: +"Bag path as supplied."
      • removedOutput schema / properties / path / title
        Removed value: -"Path"
      • removedOutput schema / properties / recording_duration_ns
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "minimum": 0,
        -      "type": "integer"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "Recording duration in nanoseconds when readable from the bag's index. `None` when only `ros2 bag info` text was parsed; `duration_seconds` (float) is the always-populated fallback that downstream LLM consumers should prefer when this is `None`.",
        -  "title": "Recording Duration Ns"
        -}
      • removedOutput schema / properties / samples_decoded_count
        Removed value: -{
        -  "default": 0,
        -  "description": "Total decoded sample count across all topics produced by the bag reader. `0` when the reader only parsed metadata or when `rosbags` is not installed on the host. Use `peek_bag_samples` to pull the actual sample payloads for a specific topic.",
        -  "minimum": 0,
        -  "title": "Samples Decoded Count",
        -  "type": "integer"
        -}
      • removedOutput schema / properties / storage_format / title
        Removed value: -"Storage Format"
      • removedOutput schema / properties / topics / title
        Removed value: -"Topics"
      • changedOutput schema / required
        Previous value: -[
        -  "path",
        -  "duration_seconds",
        -  "message_count",
        -  "topics",
        -  "mode_effective"
        -]New value: +[
        +  "path",
        +  "duration_s",
        +  "message_count",
        +  "topics",
        +  "mode_effective"
        +]
    • Changeddetect_qos_mismatches110 fields changed
      • changedInput schema / properties / topic / description
        Previous value: -"Optional DDS topic name to scope the scan to: a bare name such as `scan` or a ROS 2 mangled name such as `rt/scan`. Omit to scan all topics."New value: +"Optional DDS topic to scope the scan to: `scan` or `rt/scan`. Omit to scan all topics."
      • removedInput schema / properties / topic / title
        Removed value: -"Topic"
      • removedInput schema / title
        Removed value: -"detect_qos_mismatchesArguments"
      • removedOutput schema / $defs / MatchedPair / description
        Removed value: -"A reader and a writer that DDS will connect given their announced QoS."
      • changedOutput schema / $defs / MatchedPair / properties / late_joiner / description
        Previous value: -"True when the writer is VOLATILE and the reader was announced more than 1 s after it, both on the same host: samples published before the reader joined are not delivered to it. Normal for a VOLATILE writer, not a fault."New value: +"True for a VOLATILE writer whose reader joined over 1 s later on the same host: earlier samples are not delivered. Normal."
      • removedOutput schema / $defs / MatchedPair / properties / late_joiner / title
        Removed value: -"Late Joiner"
      • removedOutput schema / $defs / MatchedPair / properties / late_joiner_note / title
        Removed value: -"Late Joiner Note"
      • removedOutput schema / $defs / MatchedPair / properties / reader_guid / description
        Removed value: -"GUID of the reader endpoint."
      • removedOutput schema / $defs / MatchedPair / properties / reader_guid / title
        Removed value: -"Reader Guid"
      • removedOutput schema / $defs / MatchedPair / properties / reader_participant_guid / description
        Removed value: -"GUID of the reader's participant."
      • removedOutput schema / $defs / MatchedPair / properties / reader_participant_guid / title
        Removed value: -"Reader Participant Guid"
      • removedOutput schema / $defs / MatchedPair / properties / reader_participant_name / description
        Removed value: -"Announced name of the reader's participant."
      • removedOutput schema / $defs / MatchedPair / properties / reader_participant_name / title
        Removed value: -"Reader Participant Name"
      • removedOutput schema / $defs / MatchedPair / properties / topic / description
        Removed value: -"Topic name both endpoints use."
      • removedOutput schema / $defs / MatchedPair / properties / topic / title
        Removed value: -"Topic"
      • removedOutput schema / $defs / MatchedPair / properties / type_name / description
        Removed value: -"Type name both announced."
      • removedOutput schema / $defs / MatchedPair / properties / type_name / title
        Removed value: -"Type Name"
      • removedOutput schema / $defs / MatchedPair / properties / writer_guid / description
        Removed value: -"GUID of the writer endpoint."
      • removedOutput schema / $defs / MatchedPair / properties / writer_guid / title
        Removed value: -"Writer Guid"
      • removedOutput schema / $defs / MatchedPair / properties / writer_participant_guid / description
        Removed value: -"GUID of the writer's participant."
      • removedOutput schema / $defs / MatchedPair / properties / writer_participant_guid / title
        Removed value: -"Writer Participant Guid"
      • removedOutput schema / $defs / MatchedPair / properties / writer_participant_name / description
        Removed value: -"Announced name of the writer's participant."
      • removedOutput schema / $defs / MatchedPair / properties / writer_participant_name / title
        Removed value: -"Writer Participant Name"
      • removedOutput schema / $defs / MatchedPair / title
        Removed value: -"MatchedPair"
      • removedOutput schema / $defs / MismatchReport / description
        Removed value: -"A single reader/writer QoS incompatibility detected on a topic.\n\nOnly pairs that share a partition and a type name are reported here: those\nseparated by partition or type are `NotMatchedPair`s in the same scan."
      • changedOutput schema / $defs / MismatchReport / properties / details / description
        Previous value: -"Requested and offered value and the failed rule, one entry per policy."New value: +"Requested and offered value and the failed rule, per policy."
      • removedOutput schema / $defs / MismatchReport / properties / details / title
        Removed value: -"Details"
      • changedOutput schema / $defs / MismatchReport / properties / incompatible_policies / description
        Previous value: -"Names of the QoS policies that block communication or risk degradation: see `details` for the compared values."New value: +"Blocking or risky policies; values in `details`."
      • removedOutput schema / $defs / MismatchReport / properties / incompatible_policies / title
        Removed value: -"Incompatible Policies"
      • removedOutput schema / $defs / MismatchReport / properties / mode_effective
        Removed value: -{
        -  "description": "Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`.",
        -  "enum": [
        -    "mock",
        -    "live"
        -  ],
        -  "title": "Mode Effective",
        -  "type": "string"
        -}
      • removedOutput schema / $defs / MismatchReport / properties / reader_guid / description
        Removed value: -"GUID of the reader endpoint involved in the mismatch, if known."
      • removedOutput schema / $defs / MismatchReport / properties / reader_guid / title
        Removed value: -"Reader Guid"
      • removedOutput schema / $defs / MismatchReport / properties / reader_participant_guid / description
        Removed value: -"GUID of the participant that owns the reader, if known."
      • removedOutput schema / $defs / MismatchReport / properties / reader_participant_guid / title
        Removed value: -"Reader Participant Guid"
      • removedOutput schema / $defs / MismatchReport / properties / reader_participant_name / description
        Removed value: -"Announced name of the reader's participant, if it set one."
      • removedOutput schema / $defs / MismatchReport / properties / reader_participant_name / title
        Removed value: -"Reader Participant Name"
      • removedOutput schema / $defs / MismatchReport / properties / reader_type_name / description
        Removed value: -"Type name the reader announced."
      • removedOutput schema / $defs / MismatchReport / properties / reader_type_name / title
        Removed value: -"Reader Type Name"
      • changedOutput schema / $defs / MismatchReport / properties / severity / description
        Previous value: -"`incompatible` means communication is definitely blocked; `risky` means it may degrade but is not strictly blocked by the DDS spec (History only). Useful for an LLM to triage user-facing advice."New value: +"`incompatible` (blocked) or `risky` (may degrade; History only)."
      • removedOutput schema / $defs / MismatchReport / properties / severity / title
        Removed value: -"Severity"
      • addedOutput schema / $defs / MismatchReport / properties / suggested_fixes
        Added value: +{
        +  "description": "Fixes as text for a human to apply, never applied; empty when none.",
        +  "items": {
        +    "$ref": "#/$defs/SuggestedFix"
        +  },
        +  "type": "array"
        +}
      • removedOutput schema / $defs / MismatchReport / properties / topic / description
        Removed value: -"Topic name where the mismatch was detected."
      • removedOutput schema / $defs / MismatchReport / properties / topic / title
        Removed value: -"Topic"
      • changedOutput schema / $defs / MismatchReport / properties / unchecked / description
        Previous value: -"Policies that could not be compared for this pair because one side did not announce a value."New value: +"Policies not compared because one side announced no value."
      • removedOutput schema / $defs / MismatchReport / properties / unchecked / title
        Removed value: -"Unchecked"
      • removedOutput schema / $defs / MismatchReport / properties / writer_guid / description
        Removed value: -"GUID of the writer endpoint involved in the mismatch, if known."
      • removedOutput schema / $defs / MismatchReport / properties / writer_guid / title
        Removed value: -"Writer Guid"
      • removedOutput schema / $defs / MismatchReport / properties / writer_participant_guid / description
        Removed value: -"GUID of the participant that owns the writer, if known."
      • removedOutput schema / $defs / MismatchReport / properties / writer_participant_guid / title
        Removed value: -"Writer Participant Guid"
      • removedOutput schema / $defs / MismatchReport / properties / writer_participant_name / description
        Removed value: -"Announced name of the writer's participant, if it set one."
      • removedOutput schema / $defs / MismatchReport / properties / writer_participant_name / title
        Removed value: -"Writer Participant Name"
      • removedOutput schema / $defs / MismatchReport / properties / writer_type_name / description
        Removed value: -"Type name the writer announced."
      • removedOutput schema / $defs / MismatchReport / properties / writer_type_name / title
        Removed value: -"Writer Type Name"
      • changedOutput schema / $defs / MismatchReport / required
        Previous value: -[
        -  "topic",
        -  "incompatible_policies",
        -  "severity",
        -  "mode_effective"
        -]New value: +[
        +  "topic",
        +  "incompatible_policies",
        +  "severity"
        +]
      • removedOutput schema / $defs / MismatchReport / title
        Removed value: -"MismatchReport"
      • removedOutput schema / $defs / NotMatchedPair / description
        Removed value: -"A reader and a writer on one topic that DDS will not match, whatever their QoS."
      • removedOutput schema / $defs / NotMatchedPair / properties / detail / title
        Removed value: -"Detail"
      • changedOutput schema / $defs / NotMatchedPair / properties / latent_incompatible_policies / description
        Previous value: -"RxO policies that would ALSO be incompatible once the partition / type issue is fixed (same shape as `MismatchReport.details`). Empty when the QoS of the two endpoints would be compatible, or could not be compared."New value: +"RxO policies that would ALSO be incompatible once the partition or type is fixed (shape of `MismatchReport.details`)."
      • removedOutput schema / $defs / NotMatchedPair / properties / latent_incompatible_policies / title
        Removed value: -"Latent Incompatible Policies"
      • removedOutput schema / $defs / NotMatchedPair / properties / reader_guid / description
        Removed value: -"GUID of the reader endpoint."
      • removedOutput schema / $defs / NotMatchedPair / properties / reader_guid / title
        Removed value: -"Reader Guid"
      • removedOutput schema / $defs / NotMatchedPair / properties / reader_participant_guid / description
        Removed value: -"GUID of the reader's participant."
      • removedOutput schema / $defs / NotMatchedPair / properties / reader_participant_guid / title
        Removed value: -"Reader Participant Guid"
      • removedOutput schema / $defs / NotMatchedPair / properties / reader_participant_name / description
        Removed value: -"Announced name of the reader's participant."
      • removedOutput schema / $defs / NotMatchedPair / properties / reader_participant_name / title
        Removed value: -"Reader Participant Name"
      • changedOutput schema / $defs / NotMatchedPair / properties / reason / description
        Previous value: -"`partition`: no reader partition matches a writer partition. `type_name`: the endpoints announced different type names. The RxO QoS rules are not evaluated for such a pair."New value: +"`partition` (none in common) or `type_name` (different types); QoS rules are not evaluated."
      • removedOutput schema / $defs / NotMatchedPair / properties / reason / title
        Removed value: -"Reason"
      • addedOutput schema / $defs / NotMatchedPair / properties / suggested_fixes
        Added value: +{
        +  "description": "Fixes proposed as text for a human to apply; TopicForge never applies them. Only for a `partition` split, and only on the first pair of identical splits on a topic. Empty when it proposes none for this finding.",
        +  "items": {
        +    "$ref": "#/$defs/SuggestedFix"
        +  },
        +  "type": "array"
        +}
      • removedOutput schema / $defs / NotMatchedPair / properties / topic / description
        Removed value: -"Topic name both endpoints use."
      • removedOutput schema / $defs / NotMatchedPair / properties / topic / title
        Removed value: -"Topic"
      • removedOutput schema / $defs / NotMatchedPair / properties / writer_guid / description
        Removed value: -"GUID of the writer endpoint."
      • removedOutput schema / $defs / NotMatchedPair / properties / writer_guid / title
        Removed value: -"Writer Guid"
      • removedOutput schema / $defs / NotMatchedPair / properties / writer_participant_guid / description
        Removed value: -"GUID of the writer's participant."
      • removedOutput schema / $defs / NotMatchedPair / properties / writer_participant_guid / title
        Removed value: -"Writer Participant Guid"
      • removedOutput schema / $defs / NotMatchedPair / properties / writer_participant_name / description
        Removed value: -"Announced name of the writer's participant."
      • removedOutput schema / $defs / NotMatchedPair / properties / writer_participant_name / title
        Removed value: -"Writer Participant Name"
      • removedOutput schema / $defs / NotMatchedPair / title
        Removed value: -"NotMatchedPair"
      • removedOutput schema / $defs / PolicyMismatch / description
        Removed value: -"One QoS policy of a reader/writer pair, with the values that were compared."
      • removedOutput schema / $defs / PolicyMismatch / properties / offered / description
        Removed value: -"What the writer offers, same format as `requested`."
      • removedOutput schema / $defs / PolicyMismatch / properties / offered / title
        Removed value: -"Offered"
      • changedOutput schema / $defs / PolicyMismatch / properties / policy / description
        Previous value: -"Policy name: `Reliability`, `Durability`, `Deadline`, `Liveliness`, `LatencyBudget`, `Ownership`, `DestinationOrder`, `DataRepresentation` or `History`."New value: +"Policy name, e.g. `Reliability`, `Deadline`, `Ownership`, `History`."
      • removedOutput schema / $defs / PolicyMismatch / properties / policy / title
        Removed value: -"Policy"
      • changedOutput schema / $defs / PolicyMismatch / properties / requested / description
        Previous value: -"What the reader requests, human-readable: `\"RELIABLE\"`, `\"100 ms\"`, `\"infinite\"`."New value: +"What the reader requests, e.g. `\"RELIABLE\"`, `\"100 ms\"`."
      • removedOutput schema / $defs / PolicyMismatch / properties / requested / title
        Removed value: -"Requested"
      • removedOutput schema / $defs / PolicyMismatch / properties / rule / title
        Removed value: -"Rule"
      • removedOutput schema / $defs / PolicyMismatch / title
        Removed value: -"PolicyMismatch"
      • addedOutput schema / $defs / SuggestedFix
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "kind": {
        +      "description": "Form of `text`: a CLI command, a ROS 2 QoS override, a DDS profile or a launch parameter.",
        +      "enum": [
        +        "cli",
        +        "qos_override",
        +        "dds_profile",
        +        "launch_param"
        +      ],
        +      "type": "string"
        +    },
        +    "target": {
        +      "description": "Which node or side to change, writer or reader.",
        +      "type": "string"
        +    },
        +    "text": {
        +      "description": "The exact command, YAML or XML to apply.",
        +      "type": "string"
        +    },
        +    "tradeoff": {
        +      "description": "What is lost by applying it.",
        +      "type": "string"
        +    },
        +    "verify_with": {
        +      "description": "The TopicForge tool to run again to confirm the repair.",
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "kind",
        +    "target",
        +    "text",
        +    "tradeoff",
        +    "verify_with"
        +  ],
        +  "type": "object"
        +}
      • changedOutput schema / properties / hints / description
        Previous value: -"Leads that are not findings: orphan topics with near-identical names (typos), type id differences, pairs that could not be fully checked."New value: +"Leads that are not findings: probable typos, type id notes, incomplete checks."
      • removedOutput schema / properties / hints / title
        Removed value: -"Hints"
      • changedOutput schema / properties / matched / description
        Previous value: -"Pairs that will be matched by DDS given the announced QoS: same topic and type name, overlapping partitions, no incompatible RxO policy (a pair with only a `risky` History finding still counts). Actual data flow is not observed."New value: +"Pairs DDS will connect given the announced QoS (data flow is not observed)."
      • removedOutput schema / properties / matched / title
        Removed value: -"Matched"
      • removedOutput schema / properties / matched_total / title
        Removed value: -"Matched Total"
      • changedOutput schema / properties / mode_effective / description
        Previous value: -"Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`."New value: +"`live` or `mock` (fixtures)."
      • removedOutput schema / properties / mode_effective / title
        Removed value: -"Mode Effective"
      • changedOutput schema / properties / not_matched / description
        Previous value: -"Pairs separated by partition or type name. No data flows between them. An empty `reports` with a non-empty `not_matched` does not mean the bus is healthy."New value: +"Pairs DDS never matches (partition or type name). Non-empty with empty `reports` is NOT all-clear: no data flows between them."
      • removedOutput schema / properties / not_matched / title
        Removed value: -"Not Matched"
      • removedOutput schema / properties / not_matched_total / title
        Removed value: -"Not Matched Total"
      • addedOutput schema / properties / note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One sentence of context, or `None`."
        +}
      • changedOutput schema / properties / pairs_checked / description
        Previous value: -"Same-topic (reader, writer) pairs examined, including those reported in `not_matched`."New value: +"Same-topic (reader, writer) pairs examined."
      • removedOutput schema / properties / pairs_checked / title
        Removed value: -"Pairs Checked"
      • removedOutput schema / properties / policies_checked / title
        Removed value: -"Policies Checked"
      • changedOutput schema / properties / policies_unchecked / description
        Previous value: -"Policies and facts this scan does not cover, each with a one-line reason. A clean result says nothing about them."New value: +"What the scan does not cover, each with a reason; a clean result says nothing about them."
      • removedOutput schema / properties / policies_unchecked / title
        Removed value: -"Policies Unchecked"
      • changedOutput schema / properties / reports / description
        Previous value: -"Pairs that share a partition and a type but have incompatible or risky QoS."New value: +"Pairs sharing partition and type with incompatible or risky QoS."
      • removedOutput schema / properties / reports / title
        Removed value: -"Reports"
      • removedOutput schema / properties / reports_total / title
        Removed value: -"Reports Total"
      • addedOutput schema / properties / summary
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One-line reading of the scan: counts of incompatible, not matched and matched pairs."
        +}
      • removedOutput schema / properties / topics_scanned / title
        Removed value: -"Topics Scanned"
      • changedOutput schema / properties / truncated / description
        Previous value: -"True when `reports`, `matched` or `not_matched` was cut to its cap (200 entries each, incompatible reports first): see the `*_total` fields. Narrow the scan with `topic`."New value: +"True when `reports`, `matched` or `not_matched` hit the 200-entry cap (see `*_total`); narrow with `topic`."
      • removedOutput schema / properties / truncated / title
        Removed value: -"Truncated"
    • Addedget_node_info
    • Changedget_topic_info28 fields changed
      • changedInput schema / properties / topic / description
        Previous value: -"Fully qualified ROS2 topic name starting with `/`, e.g. `/cmd_vel` or `/camera/image_raw`. Each `/`-separated segment must start with a letter or underscore and contain only letters, digits, and underscores; everything else (whitespace, quotes, shell metacharacters, `//`, trailing `/`) is rejected before reaching the `ros2` CLI."New value: +"Fully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI."
      • removedInput schema / properties / topic / title
        Removed value: -"Topic"
      • removedInput schema / title
        Removed value: -"get_topic_infoArguments"
      • removedOutput schema / $defs / QosProfile
        Removed value: -{
        -  "additionalProperties": false,
        -  "description": "QoS profile of one DDS endpoint (reader or writer).\n\nCovers the policies behind most \"subscriber receives nothing\" cases.\nVendor-specific extensions are ignored.",
        -  "properties": {
        -    "data_representation": {
        -      "anyOf": [
        -        {
        -          "items": {
        -            "type": "string"
        -          },
        -          "type": "array"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Data representations the endpoint accepts or offers, from `XCDR1` and `XCDR2`. `None` when not announced.",
        -      "title": "Data Representation"
        -    },
        -    "deadline_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Deadline QoS in nanoseconds. `None` means no deadline (infinite, the DDS default). A reader deadline tighter (smaller) than a writer deadline is incompatible: the writer cannot guarantee the reader's promise.",
        -      "title": "Deadline Ns"
        -    },
        -    "destination_order": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "BY_RECEPTION_TIMESTAMP",
        -            "BY_SOURCE_TIMESTAMP"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "DestinationOrder QoS. `None` when not announced.",
        -      "title": "Destination Order"
        -    },
        -    "durability": {
        -      "description": "DDS Durability QoS. `VOLATILE` writers do not retain samples for late joiners; `TRANSIENT_LOCAL` writers do. A `TRANSIENT_LOCAL` reader cannot match a `VOLATILE` writer.",
        -      "enum": [
        -        "VOLATILE",
        -        "TRANSIENT_LOCAL",
        -        "TRANSIENT",
        -        "PERSISTENT"
        -      ],
        -      "title": "Durability",
        -      "type": "string"
        -    },
        -    "history": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "KEEP_LAST",
        -            "KEEP_ALL"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "DDS History QoS. `KEEP_LAST` keeps a bounded ring buffer of size `history_depth`; `KEEP_ALL` keeps every sample (memory permitting). Mixed `KEEP_ALL` reader with `KEEP_LAST` writer is risky but not strictly incompatible. `None` when it is not known: DDS discovery does not carry History, so it is reported only for TopicForge's own endpoints and for Cyclone DDS peers that set something other than the default (see `history_note`).",
        -      "title": "History"
        -    },
        -    "history_depth": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Depth for `KEEP_LAST`. `None` for `KEEP_ALL` or when `history` is `None`.",
        -      "title": "History Depth"
        -    },
        -    "history_note": {
        -      "anyOf": [
        -        {
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Set when `history` was left out because discovery did not announce it.",
        -      "title": "History Note"
        -    },
        -    "latency_budget_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "LatencyBudget QoS in nanoseconds (a delivery-delay hint). `0` is the DDS default. `None` means infinite or not announced.",
        -      "title": "Latency Budget Ns"
        -    },
        -    "liveliness_kind": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "AUTOMATIC",
        -            "MANUAL_BY_PARTICIPANT",
        -            "MANUAL_BY_TOPIC"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Liveliness QoS kind. `AUTOMATIC` is asserted by the middleware; the two `MANUAL_*` kinds need the application to write or assert liveliness, and the endpoint is declared not alive when it does not within `liveliness_lease_ns`. `None` when not announced.",
        -      "title": "Liveliness Kind"
        -    },
        -    "liveliness_lease_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Liveliness lease duration in nanoseconds. `None` means infinite (the DDS default) or not announced.",
        -      "title": "Liveliness Lease Ns"
        -    },
        -    "ownership_kind": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "SHARED",
        -            "EXCLUSIVE"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Ownership QoS kind. With `EXCLUSIVE`, only the writer with the highest `ownership_strength` delivers samples to a reader. `None` when not announced.",
        -      "title": "Ownership Kind"
        -    },
        -    "ownership_strength": {
        -      "anyOf": [
        -        {
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Ownership strength of a writer (`EXCLUSIVE` ownership). `None` when not announced.",
        -      "title": "Ownership Strength"
        -    },
        -    "partitions": {
        -      "anyOf": [
        -        {
        -          "items": {
        -            "type": "string"
        -          },
        -          "type": "array"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Partition names the endpoint's Publisher or Subscriber belongs to. No Partition policy is the default partition, reported as `[\"\"]` (never an empty list). Endpoints only match when their partitions intersect. `None` only when the backend cannot read partitions at all (unknown, not the default).",
        -      "title": "Partitions"
        -    },
        -    "reliability": {
        -      "description": "DDS Reliability QoS. `RELIABLE` retries lost samples; `BEST_EFFORT` does not. A `RELIABLE` reader cannot match a `BEST_EFFORT` writer.",
        -      "enum": [
        -        "RELIABLE",
        -        "BEST_EFFORT"
        -      ],
        -      "title": "Reliability",
        -      "type": "string"
        -    },
        -    "unknown_policies": {
        -      "anyOf": [
        -        {
        -          "items": {
        -            "type": "string"
        -          },
        -          "type": "array"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Policies the endpoint announced but whose value could not be read (for example `Deadline`). Their duration fields are `None` here, which does NOT mean infinite for these: the scan does not compare them.",
        -      "title": "Unknown Policies"
        -    }
        -  },
        -  "required": [
        -    "reliability",
        -    "durability"
        -  ],
        -  "title": "QosProfile",
        -  "type": "object"
        -}
      • addedOutput schema / $defs / SideQos
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "durability": {
        +      "description": "Shared durability of this side, or `mixed`; `transient_local` marks a latched topic such as `/tf_static`.",
        +      "enum": [
        +        "volatile",
        +        "transient_local",
        +        "mixed"
        +      ],
        +      "type": "string"
        +    },
        +    "endpoint_count": {
        +      "description": "Endpoints of this side the values were read from.",
        +      "minimum": 1,
        +      "type": "integer"
        +    },
        +    "reliability": {
        +      "description": "Reliability shared by the endpoints of this side, or `mixed` when they disagree.",
        +      "enum": [
        +        "reliable",
        +        "best_effort",
        +        "mixed"
        +      ],
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "reliability",
        +    "durability",
        +    "endpoint_count"
        +  ],
        +  "type": "object"
        +}
      • changedOutput schema / description
        Previous value: -"Description of a single ROS2 topic.\n\nCarries optional DDS-side enrichment fields when the active middleware\nbackend can resolve them (CycloneDDS / Fast DDS). The ROS2 CLI adapter and\nthe mock ROS2 path leave them `None`."New value: +"Detail of a single ROS2 topic: counts, QoS per side and the nodes on each side."
      • removedOutput schema / properties / message_type / description
        Removed value: -"ROS2 message type, e.g. `geometry_msgs/msg/Twist`."
      • removedOutput schema / properties / message_type / title
        Removed value: -"Message Type"
      • changedOutput schema / properties / mode_effective / description
        Previous value: -"Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`."New value: +"`live` or `mock` (fixtures)."
      • removedOutput schema / properties / mode_effective / title
        Removed value: -"Mode Effective"
      • removedOutput schema / properties / name / description
        Removed value: -"Fully qualified topic name, e.g. `/cmd_vel`."
      • removedOutput schema / properties / name / title
        Removed value: -"Name"
      • addedOutput schema / properties / note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One sentence of context, or `None`."
        +}
      • removedOutput schema / properties / publisher_count / description
        Removed value: -"Publishers known to the graph."
      • removedOutput schema / properties / publisher_count / title
        Removed value: -"Publisher Count"
      • addedOutput schema / properties / publisher_nodes
        Added value: +{
        +  "description": "Publisher node names (`ros2 topic info --verbose`); empty when unknown.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / publisher_qos
        Added value: +{
        +  "anyOf": [
        +    {
        +      "$ref": "#/$defs/SideQos"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "QoS of the publishers. `None` when it was not read: see `publisher_qos_note`."
        +}
      • addedOutput schema / properties / publisher_qos_note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Why `publisher_qos` is `None`. `None` when it is set."
        +}
      • removedOutput schema / properties / qos_durability
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "type": "string"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "Durability announced by the topic's publishers: `volatile`, `transient_local` (late subscribers receive the last samples; typical of latched topics such as `/tf_static`), or `mixed` when publishers disagree. `null` when unknown, with the same rules as `qos_reliability`.",
        -  "title": "Qos Durability"
        -}
      • removedOutput schema / properties / qos_profile
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "$ref": "#/$defs/QosProfile"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "Effective DDS QoS profile for this topic when resolvable. `None` from the ROS2 CLI adapter or when the DDS module is inactive. The DDS module populates this on a best-effort basis (picks one representative endpoint if reader/writer QoS differ)."
        -}
      • removedOutput schema / properties / qos_reliability
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "type": "string"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "Reliability announced by the topic's publishers: `reliable`, `best_effort`, or `mixed` when publishers disagree. `null` when unknown: the topic has no publisher, or the value was not read (`list_topics` does not read QoS; `get_topic_info` does).",
        -  "title": "Qos Reliability"
        -}
      • removedOutput schema / properties / reader_count
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "minimum": 0,
        -      "type": "integer"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "DDS reader-endpoint count when the active backend can resolve endpoint-level info (Cyclone / Fast DDS). `None` from the ROS2 CLI adapter or when the DDS module is inactive.",
        -  "title": "Reader Count"
        -}
      • removedOutput schema / properties / subscriber_count / description
        Removed value: -"Subscribers known to the graph."
      • removedOutput schema / properties / subscriber_count / title
        Removed value: -"Subscriber Count"
      • addedOutput schema / properties / subscriber_nodes
        Added value: +{
        +  "description": "Subscriber node names; empty when unknown.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / subscription_qos
        Added value: +{
        +  "anyOf": [
        +    {
        +      "$ref": "#/$defs/SideQos"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "QoS of the subscriptions. `None` when it was not read: see `subscription_qos_note`."
        +}
      • addedOutput schema / properties / subscription_qos_note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Why `subscription_qos` is `None`. `None` when it is set."
        +}
      • removedOutput schema / properties / writer_count
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "minimum": 0,
        -      "type": "integer"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "DDS writer-endpoint count when the active backend can resolve endpoint-level info. `None` from the ROS2 CLI adapter or when the DDS module is inactive.",
        -  "title": "Writer Count"
        -}
    • Changedhealth_check50 fields changed
      • removedInput schema / title
        Removed value: -"health_checkArguments"
      • addedOutput schema / properties / contract_version
        Added value: +{
        +  "default": 2,
        +  "description": "Output contract version; changes only when a field is renamed, removed or retyped.",
        +  "minimum": 1,
        +  "type": "integer"
        +}
      • changedOutput schema / properties / dds_backend / description
        Previous value: -"DDS backend of the adapter actually serving requests. `none` when the DDS module is not active (default for ROS2-only installs). `mock` for synthetic fixtures. `cyclone` requires `pip install \"topicforge[dds-cyclone]\"` (Eclipse CycloneDDS); `fast` requires a Fast DDS Python binding built from eProsima sources (not on PyPI); `opendds` and `dust` are permanent stub adapters that never serve."New value: +"DDS backend serving: `mock`, `cyclone`, `fast`, or `none` (DDS module inactive)."
      • changedOutput schema / properties / dds_backend / enum
        Previous value: -[
        -  "mock",
        -  "cyclone",
        -  "fast",
        -  "opendds",
        -  "dust",
        -  "none"
        -]New value: +[
        +  "mock",
        +  "cyclone",
        +  "fast",
        +  "none"
        +]
      • removedOutput schema / properties / dds_backend / title
        Removed value: -"Dds Backend"
      • removedOutput schema / properties / dds_domain_id / title
        Removed value: -"Dds Domain Id"
      • addedOutput schema / properties / dds_inactive_note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Why `dds_backend` is `none` while ROS 2 serves (not selected, binding missing, or failed to start); else `null`."
        +}
      • removedOutput schema / properties / dds_inactive_reason
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "type": "string"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "Why `dds_backend` is `none` while the ROS 2 CLI serves: the backend was not selected (`TOPICFORGE_DDS_BACKEND` unset or `mock`), its Python binding is not installed, or the binding is installed but the adapter failed to start. `null` when a DDS backend is serving or the cause is not known.",
        -  "title": "Dds Inactive Reason"
        -}
      • changedOutput schema / properties / dds_security / description
        Previous value: -"DDS Security is not handled. On a secured domain TopicForge can show participants but not protected endpoints or data."New value: +"DDS Security is not handled: protected endpoints and data are invisible."
      • removedOutput schema / properties / dds_security / title
        Removed value: -"Dds Security"
      • changedOutput schema / properties / max_sample_count / description
        Previous value: -"Server-side cap on the number of samples returned per `sample_messages` call. Requests above this limit are silently clamped; the value is exposed here so a client can size its requests proactively. Constant within a given server version."New value: +"Server cap on samples per call; larger requests are clamped."
      • removedOutput schema / properties / max_sample_count / title
        Removed value: -"Max Sample Count"
      • changedOutput schema / properties / middleware_available / description
        Previous value: -"True when a DDS backend is serving (`dds_backend` is not `none`). When the DDS module is inactive (`dds_backend == 'none'`), whether the *configured* backend's Python bindings are importable, so a missing binding is visible."New value: +"True when a DDS backend serves; else whether the configured binding is importable."
      • removedOutput schema / properties / middleware_available / title
        Removed value: -"Middleware Available"
      • changedOutput schema / properties / mode / description
        Previous value: -"Runtime mode of the adapter actually serving requests: `mock` or `live`. `live` with `ros_backend` `none` means the DDS tools are live and the ROS 2 tools are not available. Can differ from `requested_mode` when a live backend could not start (e.g. `live` requested without `ros2` installed falls back to `mock`)."New value: +"Mode actually serving: `mock` or `live` (`live` with `ros_backend` `none`: DDS tools only). May differ from `requested_mode` after a fallback."
      • removedOutput schema / properties / mode / title
        Removed value: -"Mode"
      • addedOutput schema / properties / note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One sentence of context, or `None`."
        +}
      • removedOutput schema / properties / now_ns / title
        Removed value: -"Now Ns"
      • changedOutput schema / properties / observed_domain_note / description
        Previous value: -"Plain statement of which DDS domain is observed, set when a DDS module is active: only the domain joined at startup is visible, a program on another domain is invisible."New value: +"Which DDS domain is observed: only the one joined at startup; others are invisible."
      • removedOutput schema / properties / observed_domain_note / title
        Removed value: -"Observed Domain Note"
      • changedOutput schema / properties / observer_started_ns / description
        Previous value: -"Wall-clock time (ns since epoch) when the DDS observer joined the bus. Nothing earlier than this was watched: `now_ns` minus this is how long TopicForge has been observing. `None` without a live DDS observer."New value: +"ns when the DDS observer joined; nothing earlier was watched. `None` without one."
      • removedOutput schema / properties / observer_started_ns / title
        Removed value: -"Observer Started Ns"
      • changedOutput schema / properties / payload_decoding / description
        Previous value: -"Whether DDS user-topic payloads are decoded. `disabled` today: `peek_dds_samples` and `topic_metrics` do not return message content for user topics."New value: +"`disabled`: DDS user-topic payloads are not decoded."
      • removedOutput schema / properties / payload_decoding / title
        Removed value: -"Payload Decoding"
      • addedOutput schema / properties / payload_decoding_note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": "user-topic payload decoding is switched off until it is validated on a real bus; builtin discovery topics are still readable",
        +  "description": "Reason for `payload_decoding`."
        +}
      • removedOutput schema / properties / payload_decoding_reason
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "type": "string"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": "user-topic payload decoding is switched off until it is validated on a real bus; builtin discovery topics are still readable",
        -  "description": "One-line reason for `payload_decoding`.",
        -  "title": "Payload Decoding Reason"
        -}
      • changedOutput schema / properties / requested_mode / description
        Previous value: -"Mode requested via configuration (may be `auto`)."New value: +"Mode requested by configuration (may be `auto`)."
      • removedOutput schema / properties / requested_mode / title
        Removed value: -"Requested Mode"
      • addedOutput schema / properties / rmw_implementation
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "ROS 2 middleware, e.g. `rmw_fastrtps_cpp`; `None` when `rmw_source` is `none`."
        +}
      • addedOutput schema / properties / rmw_source
        Added value: +{
        +  "default": "none",
        +  "description": "`env`, `ros2_cli`, `distro_default` (not verified on the graph) or `none`.",
        +  "enum": [
        +    "env",
        +    "ros2_cli",
        +    "distro_default",
        +    "none"
        +  ],
        +  "type": "string"
        +}
      • removedOutput schema / properties / ros2_available / title
        Removed value: -"Ros2 Available"
      • changedOutput schema / properties / ros2_distro / description
        Previous value: -"Value of `ROS_DISTRO` if set in the environment. **Env disclosure, by design**: under the local-trust threat model (see README 'Security model'), the MCP client is a trusted agent on a machine the user controls, and exposing the ROS2 distro lets it adapt to e.g. `humble`/`jazzy` differences. For a hosted multi-tenant TopicForge endpoint this field would be scrubbed ."New value: +"`ROS_DISTRO` if set (disclosed by design under the local-trust model)."
      • removedOutput schema / properties / ros2_distro / title
        Removed value: -"Ros2 Distro"
      • changedOutput schema / properties / ros_backend / description
        Previous value: -"Active ROS2 backend. `ros2_cli` when the `ros2` CLI is on PATH and live mode resolves to a Ros2CliAdapter (alone or as the ROS half of a composite). `mock` when MockAdapter serves the ROS surface. `none` when no ROS2 backend is active (e.g. DDS-only live install with no `ros2` CLI). Together with `dds_backend` it tells the ROS2 and DDS halves of the runtime apart."New value: +"ROS 2 backend: `ros2_cli`, `mock` or `none`."
      • removedOutput schema / properties / ros_backend / title
        Removed value: -"Ros Backend"
      • changedOutput schema / properties / ros_tools_available / description
        Previous value: -"True when the ROS 2 tools (`list_topics`, `get_topic_info`, `sample_messages`, `analyze_bag`, `peek_bag_samples`) can run, i.e. `ros_backend` is not `none`. False on a DDS-only setup: use `list_endpoints` for topics and wiring there."New value: +"True when the ROS 2 tools can run (`ros_backend` is not `none`); on a DDS-only setup use `list_endpoints`."
      • removedOutput schema / properties / ros_tools_available / title
        Removed value: -"Ros Tools Available"
      • changedOutput schema / properties / server_version / description
        Previous value: -"TopicForge server version (matches the PyPI release of the `topicforge` package)."New value: +"TopicForge version."
      • removedOutput schema / properties / server_version / title
        Removed value: -"Server Version"
      • changedOutput schema / properties / sim_clock_published / description
        Previous value: -"Whether `/clock` has at least one publisher on the ROS 2 graph. True suggests nodes may run on simulated time (`use_sim_time`), so `header.stamp` values are sim time, not wall time (compare with `received_ns`). It is a hint: a publisher on `/clock` does not prove a given node follows it. `None` in mock mode or when the `ros2` CLI could not answer."New value: +"Whether `/clock` had a publisher in the last graph read (a hint that stamps may be sim time). `None` in mock or without a recent read; `health_check` never runs `ros2`."
      • removedOutput schema / properties / sim_clock_published / title
        Removed value: -"Sim Clock Published"
      • changedOutput schema / properties / tracker_cache_evictions / description
        Previous value: -"Discovery entries dropped because a tracker cache was full (4096 per cache). Non-zero means the bus is bigger than what is listed."New value: +"Discovery entries dropped from a full tracker cache (4096 each); non-zero means the bus is bigger than listed."
      • removedOutput schema / properties / tracker_cache_evictions / title
        Removed value: -"Tracker Cache Evictions"
      • changedOutput schema / properties / tracker_errors / description
        Previous value: -"Discovery tracker passes that raised (swallowed and logged). Non-zero means gaps."New value: +"Tracker passes that raised; non-zero means gaps."
      • removedOutput schema / properties / tracker_errors / title
        Removed value: -"Tracker Errors"
      • changedOutput schema / properties / tracker_last_pass_ns / description
        Previous value: -"Wall-clock time (ns since epoch) of the last completed tracker pass. A value far older than `now_ns` means lifecycle is stale."New value: +"ns of the last tracker pass; far older than `now_ns` means stale lifecycle."
      • removedOutput schema / properties / tracker_last_pass_ns / title
        Removed value: -"Tracker Last Pass Ns"
      • removedOutput schema / properties / tracker_passes / title
        Removed value: -"Tracker Passes"
      • changedOutput schema / properties / tracker_running / description
        Previous value: -"Whether the continuous discovery tracker thread is alive (Cyclone). `None` when the backend has no tracker."New value: +"Whether the discovery tracker thread is alive (Cyclone); `None` without a tracker."
      • removedOutput schema / properties / tracker_running / title
        Removed value: -"Tracker Running"
    • Changedlist_endpoints125 fields changed
      • changedInput schema / properties / domain_id / description
        Previous value: -"Accepted for compatibility (0..232). TopicForge observes the domain it joined at startup (TOPICFORGE_DDS_DOMAIN_ID); this argument does not switch domains, and the response `domain_id` says which one was observed."New value: +"Ignored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` flags a different non-zero value."
      • removedInput schema / properties / domain_id / title
        Removed value: -"Domain Id"
      • addedInput schema / properties / fields
        Added value: +{
        +  "default": "full",
        +  "description": "`full` (default) or `compact`: one-line `qos_summary` instead of the full `qos`, no `type_id`, `announced_ns` or `ros_topic_note`. Use `compact` on large buses.",
        +  "enum": [
        +    "full",
        +    "compact"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / include_departed / description
        Previous value: -"Also list endpoints whose participant left the bus (flagged with `gone_ns`). Defaults to false; `by_topic` reports them as `departed_writers` / `departed_readers` either way."New value: +"Also list endpoints whose participant left (flagged with `gone_ns`); `by_topic` reports them either way. Default false."
      • removedInput schema / properties / include_departed / title
        Removed value: -"Include Departed"
      • addedInput schema / properties / include_internal
        Added value: +{
        +  "default": false,
        +  "description": "Also list ROS 2 service and action endpoints (`rq/`, `rr/`, `rs/`, `rp/`, `ra/`, `ros_discovery_info`), most of a small robot's endpoints. Default false.",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / include_observer / description
        Previous value: -"Include TopicForge's own observer participant's endpoints. Defaults to false."New value: +"Include TopicForge's own observer endpoints. Default false."
      • removedInput schema / properties / include_observer / title
        Removed value: -"Include Observer"
      • changedInput schema / properties / participant_guid / description
        Previous value: -"Only endpoints owned by this participant, as the guid reported by `list_participants` or by an earlier `list_endpoints`. Omit for all participants."New value: +"Only endpoints of this participant (a guid from `list_participants`). Omit for all."
      • removedInput schema / properties / participant_guid / title
        Removed value: -"Participant Guid"
      • changedInput schema / properties / topic / description
        Previous value: -"Only endpoints on this DDS topic name: a bare name such as `scan` or a ROS 2 mangled name such as `rt/scan` (exact name first, then the alternate form). Omit to list every topic."New value: +"Only endpoints on this DDS topic: `scan` or `rt/scan` (exact name first, then the alternate form). Omit for every topic."
      • removedInput schema / properties / topic / title
        Removed value: -"Topic"
      • removedInput schema / title
        Removed value: -"list_endpointsArguments"
      • removedOutput schema / $defs / DepartedEndpoint / description
        Removed value: -"An endpoint whose participant left the bus (crash, clean exit or lease expiry)."
      • removedOutput schema / $defs / DepartedEndpoint / properties / gone_ns / title
        Removed value: -"Gone Ns"
      • removedOutput schema / $defs / DepartedEndpoint / properties / guid / description
        Removed value: -"GUID of the departed endpoint."
      • removedOutput schema / $defs / DepartedEndpoint / properties / guid / title
        Removed value: -"Guid"
      • removedOutput schema / $defs / DepartedEndpoint / properties / participant_guid / description
        Removed value: -"GUID of the participant that owned it."
      • removedOutput schema / $defs / DepartedEndpoint / properties / participant_guid / title
        Removed value: -"Participant Guid"
      • removedOutput schema / $defs / DepartedEndpoint / properties / participant_name / description
        Removed value: -"Announced name of that participant, `None` when it set none."
      • removedOutput schema / $defs / DepartedEndpoint / properties / participant_name / title
        Removed value: -"Participant Name"
      • removedOutput schema / $defs / DepartedEndpoint / title
        Removed value: -"DepartedEndpoint"
      • removedOutput schema / $defs / EndpointInfo / description
        Removed value: -"One DDS endpoint (writer or reader) announced through discovery."
      • removedOutput schema / $defs / EndpointInfo / properties / activity
        Removed value: -{
        -  "default": null,
        -  "description": "Reserved for a future liveness signal. Always `None` today.",
        -  "title": "Activity",
        -  "type": "null"
        -}
      • removedOutput schema / $defs / EndpointInfo / properties / activity_note
        Removed value: -{
        -  "default": "not observed: TopicForge holds no reader on user topics, so it cannot tell a silent or hung writer from a healthy one",
        -  "description": "Why `activity` is not populated.",
        -  "title": "Activity Note",
        -  "type": "string"
        -}
      • changedOutput schema / $defs / EndpointInfo / properties / announced_ns / description
        Previous value: -"Source timestamp of the discovery announcement, ns since epoch, read on the announcing side's clock (it can differ from this host's clock). `None` when not available."New value: +"Source timestamp of the announcement, on the announcer's clock; `None` if unavailable."
      • removedOutput schema / $defs / EndpointInfo / properties / announced_ns / title
        Removed value: -"Announced Ns"
      • addedOutput schema / $defs / EndpointInfo / properties / dds_topic
        Added value: +{
        +  "type": "string"
        +}
      • removedOutput schema / $defs / EndpointInfo / properties / domain_id
        Removed value: -{
        -  "description": "DDS domain the endpoint was observed on.",
        -  "maximum": 232,
        -  "minimum": 0,
        -  "title": "Domain Id",
        -  "type": "integer"
        -}
      • changedOutput schema / $defs / EndpointInfo / properties / gone_ns / description
        Previous value: -"`None` for a live endpoint. For a departed endpoint (only listed with `include_departed`): when its participant was lost (the participant's `lost_ns`, an upper bound of the death)."New value: +"`None` for a live endpoint; for a departed one (`include_departed`) when its participant was lost (upper bound)."
      • removedOutput schema / $defs / EndpointInfo / properties / gone_ns / title
        Removed value: -"Gone Ns"
      • removedOutput schema / $defs / EndpointInfo / properties / guid / description
        Removed value: -"GUID of the endpoint, `xxxxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx`."
      • removedOutput schema / $defs / EndpointInfo / properties / guid / title
        Removed value: -"Guid"
      • removedOutput schema / $defs / EndpointInfo / properties / is_observer / title
        Removed value: -"Is Observer"
      • removedOutput schema / $defs / EndpointInfo / properties / mode_effective
        Removed value: -{
        -  "description": "Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`.",
        -  "enum": [
        -    "mock",
        -    "live"
        -  ],
        -  "title": "Mode Effective",
        -  "type": "string"
        -}
      • removedOutput schema / $defs / EndpointInfo / properties / participant_guid / description
        Removed value: -"GUID of the owning participant, same format as `list_participants`."
      • removedOutput schema / $defs / EndpointInfo / properties / participant_guid / title
        Removed value: -"Participant Guid"
      • removedOutput schema / $defs / EndpointInfo / properties / participant_name / description
        Removed value: -"Announced name of the owning participant, `None` when it set none."
      • removedOutput schema / $defs / EndpointInfo / properties / participant_name / title
        Removed value: -"Participant Name"
      • removedOutput schema / $defs / EndpointInfo / properties / participant_vendor / description
        Removed value: -"Vendor of the owning participant, same value as `list_participants` (`unknown` when it cannot be determined)."
      • removedOutput schema / $defs / EndpointInfo / properties / participant_vendor / title
        Removed value: -"Participant Vendor"
      • changedOutput schema / $defs / EndpointInfo / properties / qos / description
        Previous value: -"QoS the endpoint announced. Durations (`deadline_ns`, `liveliness_lease_ns`, `latency_budget_ns`) of `None` mean infinite or not set. `None` when reliability or durability could not be resolved."New value: +"Announced QoS; `None` when reliability or durability could not be resolved. A duration of `None` is infinite or unset."
      • addedOutput schema / $defs / EndpointInfo / properties / qos_summary
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One-line QoS, set only with `fields` `compact` (where `qos` is `None`)."
        +}
      • removedOutput schema / $defs / EndpointInfo / properties / role / title
        Removed value: -"Role"
      • addedOutput schema / $defs / EndpointInfo / properties / ros_topic
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "ROS 2 name of the topic, for example `/scan`. `None` when the endpoint is not a ROS 2 topic: see `ros_topic_note`."
        +}
      • addedOutput schema / $defs / EndpointInfo / properties / ros_topic_note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Why `ros_topic` is `None`. `None` when it is set."
        +}
      • removedOutput schema / $defs / EndpointInfo / properties / topic
        Removed value: -{
        -  "description": "DDS topic name.",
        -  "title": "Topic",
        -  "type": "string"
        -}
      • removedOutput schema / $defs / EndpointInfo / properties / type_id / title
        Removed value: -"Type Id"
      • removedOutput schema / $defs / EndpointInfo / properties / type_name / description
        Removed value: -"Announced data type name."
      • removedOutput schema / $defs / EndpointInfo / properties / type_name / title
        Removed value: -"Type Name"
      • changedOutput schema / $defs / EndpointInfo / required
        Previous value: -[
        -  "guid",
        -  "role",
        -  "participant_guid",
        -  "topic",
        -  "is_observer",
        -  "domain_id",
        -  "mode_effective"
        -]New value: +[
        +  "guid",
        +  "role",
        +  "participant_guid",
        +  "dds_topic",
        +  "is_observer"
        +]
      • removedOutput schema / $defs / EndpointInfo / title
        Removed value: -"EndpointInfo"
      • removedOutput schema / $defs / QosProfile / description
        Removed value: -"QoS profile of one DDS endpoint (reader or writer).\n\nCovers the policies behind most \"subscriber receives nothing\" cases.\nVendor-specific extensions are ignored."
      • changedOutput schema / $defs / QosProfile / properties / data_representation / description
        Previous value: -"Data representations the endpoint accepts or offers, from `XCDR1` and `XCDR2`. `None` when not announced."New value: +"`XCDR1` / `XCDR2` accepted or offered; `None` when not announced."
      • removedOutput schema / $defs / QosProfile / properties / data_representation / title
        Removed value: -"Data Representation"
      • changedOutput schema / $defs / QosProfile / properties / deadline_ns / description
        Previous value: -"Deadline QoS in nanoseconds. `None` means no deadline (infinite, the DDS default). A reader deadline tighter (smaller) than a writer deadline is incompatible: the writer cannot guarantee the reader's promise."New value: +"Deadline in ns; `None` is infinite. A reader deadline shorter than the writer's is incompatible."
      • removedOutput schema / $defs / QosProfile / properties / deadline_ns / title
        Removed value: -"Deadline Ns"
      • removedOutput schema / $defs / QosProfile / properties / destination_order / title
        Removed value: -"Destination Order"
      • changedOutput schema / $defs / QosProfile / properties / durability / description
        Previous value: -"DDS Durability QoS. `VOLATILE` writers do not retain samples for late joiners; `TRANSIENT_LOCAL` writers do. A `TRANSIENT_LOCAL` reader cannot match a `VOLATILE` writer."New value: +"`VOLATILE` keeps nothing for late joiners, `TRANSIENT_LOCAL` does; a TRANSIENT_LOCAL reader cannot match a VOLATILE writer."
      • removedOutput schema / $defs / QosProfile / properties / durability / title
        Removed value: -"Durability"
      • changedOutput schema / $defs / QosProfile / properties / history / description
        Previous value: -"DDS History QoS. `KEEP_LAST` keeps a bounded ring buffer of size `history_depth`; `KEEP_ALL` keeps every sample (memory permitting). Mixed `KEEP_ALL` reader with `KEEP_LAST` writer is risky but not strictly incompatible. `None` when it is not known: DDS discovery does not carry History, so it is reported only for TopicForge's own endpoints and for Cyclone DDS peers that set something other than the default (see `history_note`)."New value: +"`KEEP_LAST` (ring of `history_depth`) or `KEEP_ALL`; `None` when discovery did not announce it (see `history_note`)."
      • removedOutput schema / $defs / QosProfile / properties / history / title
        Removed value: -"History"
      • changedOutput schema / $defs / QosProfile / properties / history_depth / description
        Previous value: -"Depth for `KEEP_LAST`. `None` for `KEEP_ALL` or when `history` is `None`."New value: +"Depth for `KEEP_LAST`; else `None`."
      • removedOutput schema / $defs / QosProfile / properties / history_depth / title
        Removed value: -"History Depth"
      • changedOutput schema / $defs / QosProfile / properties / history_note / description
        Previous value: -"Set when `history` was left out because discovery did not announce it."New value: +"Why `history` is `None`."
      • removedOutput schema / $defs / QosProfile / properties / history_note / title
        Removed value: -"History Note"
      • changedOutput schema / $defs / QosProfile / properties / latency_budget_ns / description
        Previous value: -"LatencyBudget QoS in nanoseconds (a delivery-delay hint). `0` is the DDS default. `None` means infinite or not announced."New value: +"LatencyBudget in ns (a hint); `0` is the default, `None` is infinite or not announced."
      • removedOutput schema / $defs / QosProfile / properties / latency_budget_ns / title
        Removed value: -"Latency Budget Ns"
      • changedOutput schema / $defs / QosProfile / properties / liveliness_kind / description
        Previous value: -"Liveliness QoS kind. `AUTOMATIC` is asserted by the middleware; the two `MANUAL_*` kinds need the application to write or assert liveliness, and the endpoint is declared not alive when it does not within `liveliness_lease_ns`. `None` when not announced."New value: +"`AUTOMATIC`, or `MANUAL_*` (the app must assert liveliness within `liveliness_lease_ns`); `None` when not announced."
      • removedOutput schema / $defs / QosProfile / properties / liveliness_kind / title
        Removed value: -"Liveliness Kind"
      • changedOutput schema / $defs / QosProfile / properties / liveliness_lease_ns / description
        Previous value: -"Liveliness lease duration in nanoseconds. `None` means infinite (the DDS default) or not announced."New value: +"Liveliness lease in ns; `None` is infinite or not announced."
      • removedOutput schema / $defs / QosProfile / properties / liveliness_lease_ns / title
        Removed value: -"Liveliness Lease Ns"
      • changedOutput schema / $defs / QosProfile / properties / ownership_kind / description
        Previous value: -"Ownership QoS kind. With `EXCLUSIVE`, only the writer with the highest `ownership_strength` delivers samples to a reader. `None` when not announced."New value: +"With `EXCLUSIVE` only the writer with the highest `ownership_strength` delivers; `None` when not announced."
      • removedOutput schema / $defs / QosProfile / properties / ownership_kind / title
        Removed value: -"Ownership Kind"
      • changedOutput schema / $defs / QosProfile / properties / ownership_strength / description
        Previous value: -"Ownership strength of a writer (`EXCLUSIVE` ownership). `None` when not announced."New value: +"Writer strength under `EXCLUSIVE` ownership; `None` when not announced."
      • removedOutput schema / $defs / QosProfile / properties / ownership_strength / title
        Removed value: -"Ownership Strength"
      • changedOutput schema / $defs / QosProfile / properties / partitions / description
        Previous value: -"Partition names the endpoint's Publisher or Subscriber belongs to. No Partition policy is the default partition, reported as `[\"\"]` (never an empty list). Endpoints only match when their partitions intersect. `None` only when the backend cannot read partitions at all (unknown, not the default)."New value: +"Partition names; the default partition is `[\"\"]`, never `[]`. Endpoints match only if partitions intersect; `None` when unreadable."
      • removedOutput schema / $defs / QosProfile / properties / partitions / title
        Removed value: -"Partitions"
      • changedOutput schema / $defs / QosProfile / properties / reliability / description
        Previous value: -"DDS Reliability QoS. `RELIABLE` retries lost samples; `BEST_EFFORT` does not. A `RELIABLE` reader cannot match a `BEST_EFFORT` writer."New value: +"`RELIABLE` retries lost samples, `BEST_EFFORT` does not; a RELIABLE reader cannot match a BEST_EFFORT writer."
      • removedOutput schema / $defs / QosProfile / properties / reliability / title
        Removed value: -"Reliability"
      • changedOutput schema / $defs / QosProfile / properties / unknown_policies / description
        Previous value: -"Policies the endpoint announced but whose value could not be read (for example `Deadline`). Their duration fields are `None` here, which does NOT mean infinite for these: the scan does not compare them."New value: +"Policies announced but unreadable (e.g. `Deadline`): their fields are `None`, which does NOT mean infinite; the scan does not compare them."
      • removedOutput schema / $defs / QosProfile / properties / unknown_policies / title
        Removed value: -"Unknown Policies"
      • removedOutput schema / $defs / QosProfile / title
        Removed value: -"QosProfile"
      • removedOutput schema / $defs / TopicSummary / description
        Removed value: -"Per-topic roll-up of the listed endpoints, for spotting orphans."
      • addedOutput schema / $defs / TopicSummary / properties / dds_topic
        Added value: +{
        +  "type": "string"
        +}
      • changedOutput schema / $defs / TopicSummary / properties / departed_readers / description
        Previous value: -"Readers on this topic whose participant left, newest first."New value: +"Readers whose participant left, newest first."
      • removedOutput schema / $defs / TopicSummary / properties / departed_readers / title
        Removed value: -"Departed Readers"
      • changedOutput schema / $defs / TopicSummary / properties / departed_writers / description
        Previous value: -"Writers on this topic whose participant left, newest first (bounded memory: last 200 departed endpoints, 1 h). Explains a topic that lost its only writer."New value: +"Writers whose participant left, newest first (last 200, 1 h)."
      • removedOutput schema / $defs / TopicSummary / properties / departed_writers / title
        Removed value: -"Departed Writers"
      • changedOutput schema / $defs / TopicSummary / properties / orphan / description
        Previous value: -"`no_reader`: writers but no reader. `no_writer`: readers but no writer. `None` when both sides exist."New value: +"`no_reader` (writers only), `no_writer` (readers only), else `None`."
      • removedOutput schema / $defs / TopicSummary / properties / orphan / title
        Removed value: -"Orphan"
      • removedOutput schema / $defs / TopicSummary / properties / partitions / title
        Removed value: -"Partitions"
      • removedOutput schema / $defs / TopicSummary / properties / reader_count / description
        Removed value: -"Number of listed readers."
      • removedOutput schema / $defs / TopicSummary / properties / reader_count / title
        Removed value: -"Reader Count"
      • addedOutput schema / $defs / TopicSummary / properties / ros_topic
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "ROS 2 name of the topic, for example `/scan`. `None` for a DDS topic that is not a ROS 2 topic."
        +}
      • removedOutput schema / $defs / TopicSummary / properties / topic
        Removed value: -{
        -  "description": "DDS topic name.",
        -  "title": "Topic",
        -  "type": "string"
        -}
      • removedOutput schema / $defs / TopicSummary / properties / type_names / description
        Removed value: -"Distinct type names announced on this topic."
      • removedOutput schema / $defs / TopicSummary / properties / type_names / title
        Removed value: -"Type Names"
      • removedOutput schema / $defs / TopicSummary / properties / writer_count / description
        Removed value: -"Number of listed writers."
      • removedOutput schema / $defs / TopicSummary / properties / writer_count / title
        Removed value: -"Writer Count"
      • changedOutput schema / $defs / TopicSummary / required
        Previous value: -[
        -  "topic",
        -  "type_names",
        -  "writer_count",
        -  "reader_count",
        -  "partitions"
        -]New value: +[
        +  "dds_topic",
        +  "type_names",
        +  "writer_count",
        +  "reader_count",
        +  "partitions"
        +]
      • removedOutput schema / $defs / TopicSummary / title
        Removed value: -"TopicSummary"
      • removedOutput schema / properties / by_topic / title
        Removed value: -"By Topic"
      • addedOutput schema / properties / departed_endpoint_count
        Added value: +{
        +  "default": 0,
        +  "description": "Departed endpoints matching the filters; listed in `endpoints` only with `include_departed`.",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • removedOutput schema / properties / departed_endpoints
        Removed value: -{
        -  "default": 0,
        -  "description": "Departed endpoints (their participant left) matching the filters. They are in `endpoints` only with `include_departed`; `by_topic` always carries them as `departed_writers` / `departed_readers`.",
        -  "minimum": 0,
        -  "title": "Departed Endpoints",
        -  "type": "integer"
        -}
      • removedOutput schema / properties / domain_id / description
        Removed value: -"DDS domain observed."
      • removedOutput schema / properties / domain_id / title
        Removed value: -"Domain Id"
      • removedOutput schema / properties / endpoints / title
        Removed value: -"Endpoints"
      • addedOutput schema / properties / excluded_observer_endpoint_count
        Added value: +{
        +  "default": 0,
        +  "description": "TopicForge's own endpoints left out of `endpoints` (counted in `total`).",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • removedOutput schema / properties / excluded_observer_endpoints
        Removed value: -{
        -  "default": 0,
        -  "description": "Endpoints of TopicForge's own observer participant left out of `endpoints` (they are counted in `total_discovered`). Explains `total_discovered` vs `returned` together with the filters.",
        -  "minimum": 0,
        -  "title": "Excluded Observer Endpoints",
        -  "type": "integer"
        -}
      • addedOutput schema / properties / fields
        Added value: +{
        +  "default": "full",
        +  "description": "Detail level served: `compact` drops per-endpoint QoS detail, type id and notes.",
        +  "enum": [
        +    "full",
        +    "compact"
        +  ],
        +  "type": "string"
        +}
      • addedOutput schema / properties / hidden_internal_endpoint_count
        Added value: +{
        +  "default": 0,
        +  "description": "Service/action endpoints (`rq/`, `rr/`, `rs/`, `rp/`, `ra/`, `ros_discovery_info`) left out while `include_internal` is false.",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / hints
        Added value: +{
        +  "description": "Facts true of every endpoint, stated once (e.g. liveness is not observed).",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / properties / mode_effective / description
        Previous value: -"Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`."New value: +"`live` or `mock` (fixtures)."
      • removedOutput schema / properties / mode_effective / title
        Removed value: -"Mode Effective"
      • changedOutput schema / properties / note / description
        Previous value: -"Hint when a `topic` filter matched nothing: names the closest known topics."New value: +"Set when a `topic` filter matched nothing: the closest known topics."
      • removedOutput schema / properties / note / title
        Removed value: -"Note"
      • removedOutput schema / properties / observer_guid / title
        Removed value: -"Observer Guid"
      • removedOutput schema / properties / returned / description
        Removed value: -"Length of `endpoints`."
      • removedOutput schema / properties / returned / title
        Removed value: -"Returned"
      • removedOutput schema / properties / snapshot_ns / title
        Removed value: -"Snapshot Ns"
      • addedOutput schema / properties / total
        Added value: +{
        +  "description": "Endpoints in the discovery cache before any filter.",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • removedOutput schema / properties / total_discovered
        Removed value: -{
        -  "description": "Endpoints in the discovery cache before any filter.",
        -  "minimum": 0,
        -  "title": "Total Discovered",
        -  "type": "integer"
        -}
      • removedOutput schema / properties / truncated / title
        Removed value: -"Truncated"
      • changedOutput schema / required
        Previous value: -[
        -  "domain_id",
        -  "snapshot_ns",
        -  "endpoints",
        -  "by_topic",
        -  "total_discovered",
        -  "returned",
        -  "truncated",
        -  "mode_effective"
        -]New value: +[
        +  "domain_id",
        +  "snapshot_ns",
        +  "endpoints",
        +  "by_topic",
        +  "total",
        +  "returned",
        +  "truncated",
        +  "mode_effective"
        +]
    • Addedlist_nodes
    • Changedlist_participants52 fields changed
      • changedInput schema / properties / domain_id / description
        Previous value: -"Accepted for compatibility (0..232). TopicForge observes the domain it joined at startup (TOPICFORGE_DDS_DOMAIN_ID); this argument does not switch domains, and the response `domain_id` says which one was observed."New value: +"Ignored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` flags a different non-zero value."
      • removedInput schema / properties / domain_id / title
        Removed value: -"Domain Id"
      • removedInput schema / title
        Removed value: -"list_participantsArguments"
      • addedOutput schema / $defs / NameLifecycle
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "departures": {
        +      "description": "Times a participant with this name was lost.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "lives": {
        +      "description": "Distinct GUIDs that carried this name; restarts = `lives` - 1.",
        +      "minimum": 1,
        +      "type": "integer"
        +    }
        +  },
        +  "required": [
        +    "lives",
        +    "departures"
        +  ],
        +  "type": "object"
        +}
      • removedOutput schema / $defs / ParticipantInfo / description
        Removed value: -"DDS participant discovered on the configured domain.\n\nCarries identity (`guid`, `name`, `vendor`) plus lifecycle fields\n(`first_seen_ns`, `last_seen_ns`, `status`, `seen_count`)."
      • changedOutput schema / $defs / ParticipantInfo / properties / announced_ns / description
        Previous value: -"Nanoseconds since epoch (DDS source timestamp of the participant's most recent announcement, taken from the announcing side's clock; the local clock when the announcer sent none). `None` when the backend does not expose it. Distinct from `first_seen_ns` / `last_seen_ns`, which are TopicForge's own local clock."New value: +"Source timestamp of the latest announcement, on the announcer's clock; `None` if unavailable."
      • removedOutput schema / $defs / ParticipantInfo / properties / announced_ns / title
        Removed value: -"Announced Ns"
      • removedOutput schema / $defs / ParticipantInfo / properties / domain_id / description
        Removed value: -"DDS domain id the participant is bound to."
      • removedOutput schema / $defs / ParticipantInfo / properties / domain_id / title
        Removed value: -"Domain Id"
      • changedOutput schema / $defs / ParticipantInfo / properties / first_seen_ns / description
        Previous value: -"Wall-clock timestamp (nanoseconds since epoch) of the **first** discovery sample TopicForge observed for this participant. `None` when the adapter does not track lifecycle."New value: +"Wall-clock ns of the first discovery sample seen; `None` without lifecycle tracking."
      • removedOutput schema / $defs / ParticipantInfo / properties / first_seen_ns / title
        Removed value: -"First Seen Ns"
      • removedOutput schema / $defs / ParticipantInfo / properties / guid / description
        Removed value: -"DDS Global Unique Identifier of the participant: hex string, stable across discovery events within a single deployment."
      • removedOutput schema / $defs / ParticipantInfo / properties / guid / title
        Removed value: -"Guid"
      • removedOutput schema / $defs / ParticipantInfo / properties / hostname / description
        Removed value: -"Hostname announced in discovery (Cyclone `__Hostname` property). `None` when the vendor does not announce it."
      • removedOutput schema / $defs / ParticipantInfo / properties / hostname / title
        Removed value: -"Hostname"
      • changedOutput schema / $defs / ParticipantInfo / properties / is_observer / description
        Previous value: -"True for TopicForge's own read-only observer participant, which joins the domain to watch it and appears in this list; false for every other participant."New value: +"True for TopicForge's own read-only observer participant."
      • removedOutput schema / $defs / ParticipantInfo / properties / is_observer / title
        Removed value: -"Is Observer"
      • changedOutput schema / $defs / ParticipantInfo / properties / last_seen_ns / description
        Previous value: -"Wall-clock timestamp (nanoseconds since epoch) of the **most recent** discovery sample observed. Used by `participant_events` to distinguish currently-active participants from those that left the bus. `None` when the adapter does not track lifecycle."New value: +"Wall-clock ns of the latest discovery sample; `None` without lifecycle tracking."
      • removedOutput schema / $defs / ParticipantInfo / properties / last_seen_ns / title
        Removed value: -"Last Seen Ns"
      • changedOutput schema / $defs / ParticipantInfo / properties / lost_ns / description
        Previous value: -"Nanoseconds since epoch of the moment the participant left, once `status` is `left`; `None` while it is active. How precise it is depends on `lost_time_source`. It is an upper bound of when the participant died: after a clean shutdown it is the exact leave time, after a crash it is when the lease expired, and the two cannot be told apart. The process died at or before `lost_ns`, and at most one lease earlier (10 s Cyclone default, 20 s Fast, 100 s RTI)."New value: +"Once `left`: ns upper bound of the death (exact after a clean shutdown; after a crash the lease expiry, up to one lease earlier: 10 s Cyclone, 20 s Fast, 100 s RTI). `None` while active."
      • removedOutput schema / $defs / ParticipantInfo / properties / lost_ns / title
        Removed value: -"Lost Ns"
      • changedOutput schema / $defs / ParticipantInfo / properties / lost_time_source / description
        Previous value: -"Where `lost_ns` comes from: `dds_source_timestamp` (timestamp carried by the discovery dispose) or `observed_local` (the moment TopicForge noticed, the weakest). `None` while active."New value: +"Origin of `lost_ns`: `dds_source_timestamp` or `observed_local` (weakest); `None` while active."
      • removedOutput schema / $defs / ParticipantInfo / properties / lost_time_source / title
        Removed value: -"Lost Time Source"
      • removedOutput schema / $defs / ParticipantInfo / properties / mode_effective
        Removed value: -{
        -  "description": "Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`.",
        -  "enum": [
        -    "mock",
        -    "live"
        -  ],
        -  "title": "Mode Effective",
        -  "type": "string"
        -}
      • changedOutput schema / $defs / ParticipantInfo / properties / name / description
        Previous value: -"Participant name the application announced through the standard DDS EntityName QoS (PID_ENTITY_NAME in RTPS discovery), for example `lidar_driver`. `None` when the application did not set one (Dust DDS cannot) or the backend does not expose it."New value: +"Announced EntityName (e.g. `lidar_driver`); `None` when the application set none (Dust DDS cannot)."
      • removedOutput schema / $defs / ParticipantInfo / properties / name / title
        Removed value: -"Name"
      • addedOutput schema / $defs / ParticipantInfo / properties / node_names
        Added value: +{
        +  "description": "Fully qualified ROS 2 node names sharing this participant; empty when `node_names_source` is `none`.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / $defs / ParticipantInfo / properties / node_names_source
        Added value: +{
        +  "default": "none",
        +  "description": "`ros_discovery_info`, `ros2_cli` or `none` (no link made).",
        +  "enum": [
        +    "ros_discovery_info",
        +    "ros2_cli",
        +    "none"
        +  ],
        +  "type": "string"
        +}
      • changedOutput schema / $defs / ParticipantInfo / properties / seen_count / description
        Previous value: -"Number of distinct discovery samples observed for this participant across all calls to `list_participants` during this server's lifetime. Starts at 1 and increments on each observation."New value: +"Discovery observations of this GUID in this server's lifetime. Not a restart count: a restart is a new GUID (see `restarts_by_name`)."
      • removedOutput schema / $defs / ParticipantInfo / properties / seen_count / title
        Removed value: -"Seen Count"
      • changedOutput schema / $defs / ParticipantInfo / properties / status / description
        Previous value: -"Lifecycle status. `active` means TopicForge has a recent discovery sample for this GUID and the bus has not signalled removal. `left` means the participant was observed earlier but has since disappeared (a Fast DDS `REMOVED` callback or a Cyclone polling delta). `unknown` when the adapter does not track lifecycle."New value: +"`active`, `left` (seen earlier, since gone) or `unknown` (no lifecycle tracking)."
      • removedOutput schema / $defs / ParticipantInfo / properties / status / title
        Removed value: -"Status"
      • changedOutput schema / $defs / ParticipantInfo / properties / vendor / description
        Previous value: -"DDS implementation that announced this participant, decoded from the OMG-RTPS `vendor_id` field on the discovery sample. `cyclone` (Eclipse Cyclone DDS), `fast` (eProsima Fast DDS), `rti` (RTI Connext), `rti_micro` (RTI Connext Micro), `opensplice` (ADLink OpenSplice), `opendds` (OCI OpenDDS), `coredx` (Twin Oaks CoreDX), `intercom` (Kongsberg InterCOM), `dust` (S2E Dust DDS). `mock` is reserved for synthetic fixtures. `unknown` means the vendor could not be determined: TopicForge reads it from the vendor prefix of the participant GUID, and some vendors (for example Dust DDS and RTI Connext) do not put their vendor id there, while the Cyclone Python binding does not expose the RTPS header vendor id. The participant is still reported; see `vendor_source`. Any conformant DDS-RTPS participant should appear through standard discovery; Cyclone DDS and Dust DDS have been observed on a live bus. See `docs/dds-interop-matrix.md`."New value: +"DDS implementation read from the GUID vendor prefix: cyclone, fast, rti, rti_micro, opensplice, opendds, coredx, intercom, dust, mock or `unknown` (Dust DDS and RTI Connext do not put theirs there); see `vendor_source`."
      • removedOutput schema / $defs / ParticipantInfo / properties / vendor / title
        Removed value: -"Vendor"
      • changedOutput schema / $defs / ParticipantInfo / properties / vendor_source / description
        Previous value: -"Where `vendor` came from: `guid_prefix` when it was read from the vendor prefix of the participant GUID, `none` when the vendor is unknown (or synthetic in mock mode)."New value: +"`guid_prefix` when `vendor` was read from the GUID, else `none`."
      • removedOutput schema / $defs / ParticipantInfo / properties / vendor_source / title
        Removed value: -"Vendor Source"
      • changedOutput schema / $defs / ParticipantInfo / required
        Previous value: -[
        -  "guid",
        -  "vendor",
        -  "domain_id",
        -  "mode_effective"
        -]New value: +[
        +  "guid",
        +  "vendor",
        +  "domain_id"
        +]
      • removedOutput schema / $defs / ParticipantInfo / title
        Removed value: -"ParticipantInfo"
      • addedOutput schema / additionalProperties
        Added value: +false
      • addedOutput schema / description
        Added value: +"Envelope returned by `list_participants`."
      • addedOutput schema / properties / domain_id
        Added value: +{
        +  "description": "DDS domain observed (joined at startup).",
        +  "maximum": 232,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / mode_effective
        Added value: +{
        +  "description": "`live` or `mock` (fixtures).",
        +  "enum": [
        +    "mock",
        +    "live"
        +  ],
        +  "type": "string"
        +}
      • addedOutput schema / properties / note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One sentence of context, or `None`."
        +}
      • addedOutput schema / properties / participants
        Added value: +{
        +  "items": {
        +    "$ref": "#/$defs/ParticipantInfo"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / restarts_by_name
        Added value: +{
        +  "additionalProperties": {
        +    "$ref": "#/$defs/NameLifecycle"
        +  },
        +  "description": "Names seen with several GUIDs or lost at least once (absent: ran once and stayed). A restart is a new GUID with the same name.",
        +  "type": "object"
        +}
      • addedOutput schema / properties / restarts_note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Set when unnamed participants were left out of `restarts_by_name`."
        +}
      • removedOutput schema / properties / result
        Removed value: -{
        -  "items": {
        -    "$ref": "#/$defs/ParticipantInfo"
        -  },
        -  "title": "Result",
        -  "type": "array"
        -}
      • addedOutput schema / properties / returned
        Added value: +{
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / total
        Added value: +{
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / truncated
        Added value: +{
        +  "type": "boolean"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "result"
        -]New value: +[
        +  "participants",
        +  "returned",
        +  "total",
        +  "truncated",
        +  "domain_id",
        +  "mode_effective"
        +]
      • changedOutput schema / title
        Previous value: -"list_participantsOutput"New value: +"ParticipantListing"
    • Changedlist_topics15 fields changed
      • removedInput schema / title
        Removed value: -"list_topicsArguments"
      • removedOutput schema / $defs / QosProfile
        Removed value: -{
        -  "additionalProperties": false,
        -  "description": "QoS profile of one DDS endpoint (reader or writer).\n\nCovers the policies behind most \"subscriber receives nothing\" cases.\nVendor-specific extensions are ignored.",
        -  "properties": {
        -    "data_representation": {
        -      "anyOf": [
        -        {
        -          "items": {
        -            "type": "string"
        -          },
        -          "type": "array"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Data representations the endpoint accepts or offers, from `XCDR1` and `XCDR2`. `None` when not announced.",
        -      "title": "Data Representation"
        -    },
        -    "deadline_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Deadline QoS in nanoseconds. `None` means no deadline (infinite, the DDS default). A reader deadline tighter (smaller) than a writer deadline is incompatible: the writer cannot guarantee the reader's promise.",
        -      "title": "Deadline Ns"
        -    },
        -    "destination_order": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "BY_RECEPTION_TIMESTAMP",
        -            "BY_SOURCE_TIMESTAMP"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "DestinationOrder QoS. `None` when not announced.",
        -      "title": "Destination Order"
        -    },
        -    "durability": {
        -      "description": "DDS Durability QoS. `VOLATILE` writers do not retain samples for late joiners; `TRANSIENT_LOCAL` writers do. A `TRANSIENT_LOCAL` reader cannot match a `VOLATILE` writer.",
        -      "enum": [
        -        "VOLATILE",
        -        "TRANSIENT_LOCAL",
        -        "TRANSIENT",
        -        "PERSISTENT"
        -      ],
        -      "title": "Durability",
        -      "type": "string"
        -    },
        -    "history": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "KEEP_LAST",
        -            "KEEP_ALL"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "DDS History QoS. `KEEP_LAST` keeps a bounded ring buffer of size `history_depth`; `KEEP_ALL` keeps every sample (memory permitting). Mixed `KEEP_ALL` reader with `KEEP_LAST` writer is risky but not strictly incompatible. `None` when it is not known: DDS discovery does not carry History, so it is reported only for TopicForge's own endpoints and for Cyclone DDS peers that set something other than the default (see `history_note`).",
        -      "title": "History"
        -    },
        -    "history_depth": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Depth for `KEEP_LAST`. `None` for `KEEP_ALL` or when `history` is `None`.",
        -      "title": "History Depth"
        -    },
        -    "history_note": {
        -      "anyOf": [
        -        {
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Set when `history` was left out because discovery did not announce it.",
        -      "title": "History Note"
        -    },
        -    "latency_budget_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "LatencyBudget QoS in nanoseconds (a delivery-delay hint). `0` is the DDS default. `None` means infinite or not announced.",
        -      "title": "Latency Budget Ns"
        -    },
        -    "liveliness_kind": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "AUTOMATIC",
        -            "MANUAL_BY_PARTICIPANT",
        -            "MANUAL_BY_TOPIC"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Liveliness QoS kind. `AUTOMATIC` is asserted by the middleware; the two `MANUAL_*` kinds need the application to write or assert liveliness, and the endpoint is declared not alive when it does not within `liveliness_lease_ns`. `None` when not announced.",
        -      "title": "Liveliness Kind"
        -    },
        -    "liveliness_lease_ns": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Liveliness lease duration in nanoseconds. `None` means infinite (the DDS default) or not announced.",
        -      "title": "Liveliness Lease Ns"
        -    },
        -    "ownership_kind": {
        -      "anyOf": [
        -        {
        -          "enum": [
        -            "SHARED",
        -            "EXCLUSIVE"
        -          ],
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Ownership QoS kind. With `EXCLUSIVE`, only the writer with the highest `ownership_strength` delivers samples to a reader. `None` when not announced.",
        -      "title": "Ownership Kind"
        -    },
        -    "ownership_strength": {
        -      "anyOf": [
        -        {
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Ownership strength of a writer (`EXCLUSIVE` ownership). `None` when not announced.",
        -      "title": "Ownership Strength"
        -    },
        -    "partitions": {
        -      "anyOf": [
        -        {
        -          "items": {
        -            "type": "string"
        -          },
        -          "type": "array"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Partition names the endpoint's Publisher or Subscriber belongs to. No Partition policy is the default partition, reported as `[\"\"]` (never an empty list). Endpoints only match when their partitions intersect. `None` only when the backend cannot read partitions at all (unknown, not the default).",
        -      "title": "Partitions"
        -    },
        -    "reliability": {
        -      "description": "DDS Reliability QoS. `RELIABLE` retries lost samples; `BEST_EFFORT` does not. A `RELIABLE` reader cannot match a `BEST_EFFORT` writer.",
        -      "enum": [
        -        "RELIABLE",
        -        "BEST_EFFORT"
        -      ],
        -      "title": "Reliability",
        -      "type": "string"
        -    },
        -    "unknown_policies": {
        -      "anyOf": [
        -        {
        -          "items": {
        -            "type": "string"
        -          },
        -          "type": "array"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Policies the endpoint announced but whose value could not be read (for example `Deadline`). Their duration fields are `None` here, which does NOT mean infinite for these: the scan does not compare them.",
        -      "title": "Unknown Policies"
        -    }
        -  },
        -  "required": [
        -    "reliability",
        -    "durability"
        -  ],
        -  "title": "QosProfile",
        -  "type": "object"
        -}
      • removedOutput schema / $defs / TopicInfo
        Removed value: -{
        -  "additionalProperties": false,
        -  "description": "Description of a single ROS2 topic.\n\nCarries optional DDS-side enrichment fields when the active middleware\nbackend can resolve them (CycloneDDS / Fast DDS). The ROS2 CLI adapter and\nthe mock ROS2 path leave them `None`.",
        -  "properties": {
        -    "message_type": {
        -      "description": "ROS2 message type, e.g. `geometry_msgs/msg/Twist`.",
        -      "title": "Message Type",
        -      "type": "string"
        -    },
        -    "mode_effective": {
        -      "description": "Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`.",
        -      "enum": [
        -        "mock",
        -        "live"
        -      ],
        -      "title": "Mode Effective",
        -      "type": "string"
        -    },
        -    "name": {
        -      "description": "Fully qualified topic name, e.g. `/cmd_vel`.",
        -      "title": "Name",
        -      "type": "string"
        -    },
        -    "publisher_count": {
        -      "description": "Publishers known to the graph.",
        -      "minimum": 0,
        -      "title": "Publisher Count",
        -      "type": "integer"
        -    },
        -    "qos_durability": {
        -      "anyOf": [
        -        {
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Durability announced by the topic's publishers: `volatile`, `transient_local` (late subscribers receive the last samples; typical of latched topics such as `/tf_static`), or `mixed` when publishers disagree. `null` when unknown, with the same rules as `qos_reliability`.",
        -      "title": "Qos Durability"
        -    },
        -    "qos_profile": {
        -      "anyOf": [
        -        {
        -          "$ref": "#/$defs/QosProfile"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Effective DDS QoS profile for this topic when resolvable. `None` from the ROS2 CLI adapter or when the DDS module is inactive. The DDS module populates this on a best-effort basis (picks one representative endpoint if reader/writer QoS differ)."
        -    },
        -    "qos_reliability": {
        -      "anyOf": [
        -        {
        -          "type": "string"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "Reliability announced by the topic's publishers: `reliable`, `best_effort`, or `mixed` when publishers disagree. `null` when unknown: the topic has no publisher, or the value was not read (`list_topics` does not read QoS; `get_topic_info` does).",
        -      "title": "Qos Reliability"
        -    },
        -    "reader_count": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "DDS reader-endpoint count when the active backend can resolve endpoint-level info (Cyclone / Fast DDS). `None` from the ROS2 CLI adapter or when the DDS module is inactive.",
        -      "title": "Reader Count"
        -    },
        -    "subscriber_count": {
        -      "description": "Subscribers known to the graph.",
        -      "minimum": 0,
        -      "title": "Subscriber Count",
        -      "type": "integer"
        -    },
        -    "writer_count": {
        -      "anyOf": [
        -        {
        -          "minimum": 0,
        -          "type": "integer"
        -        },
        -        {
        -          "type": "null"
        -        }
        -      ],
        -      "default": null,
        -      "description": "DDS writer-endpoint count when the active backend can resolve endpoint-level info. `None` from the ROS2 CLI adapter or when the DDS module is inactive.",
        -      "title": "Writer Count"
        -    }
        -  },
        -  "required": [
        -    "name",
        -    "message_type",
        -    "publisher_count",
        -    "subscriber_count",
        -    "mode_effective"
        -  ],
        -  "title": "TopicInfo",
        -  "type": "object"
        -}
      • addedOutput schema / $defs / TopicListItem
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "message_type": {
        +      "type": "string"
        +    },
        +    "name": {
        +      "type": "string"
        +    },
        +    "publisher_count": {
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "subscriber_count": {
        +      "minimum": 0,
        +      "type": "integer"
        +    }
        +  },
        +  "required": [
        +    "name",
        +    "message_type",
        +    "publisher_count",
        +    "subscriber_count"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / additionalProperties
        Added value: +false
      • addedOutput schema / description
        Added value: +"Envelope returned by `list_topics`."
      • addedOutput schema / properties / mode_effective
        Added value: +{
        +  "description": "`live` or `mock` (fixtures).",
        +  "enum": [
        +    "mock",
        +    "live"
        +  ],
        +  "type": "string"
        +}
      • addedOutput schema / properties / note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One sentence of context, or `None`."
        +}
      • removedOutput schema / properties / result
        Removed value: -{
        -  "items": {
        -    "$ref": "#/$defs/TopicInfo"
        -  },
        -  "title": "Result",
        -  "type": "array"
        -}
      • addedOutput schema / properties / returned
        Added value: +{
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / topics
        Added value: +{
        +  "items": {
        +    "$ref": "#/$defs/TopicListItem"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / total
        Added value: +{
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / truncated
        Added value: +{
        +  "type": "boolean"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "result"
        -]New value: +[
        +  "topics",
        +  "returned",
        +  "total",
        +  "truncated",
        +  "mode_effective"
        +]
      • changedOutput schema / title
        Previous value: -"list_topicsOutput"New value: +"TopicListing"
    • Changedparticipant_events43 fields changed
      • changedInput schema / properties / domain_id / description
        Previous value: -"Accepted for compatibility (0..232). TopicForge observes the domain it joined at startup (TOPICFORGE_DDS_DOMAIN_ID); this argument does not switch domains, and the response `domain_id` says which one was observed."New value: +"Ignored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` flags a different non-zero value."
      • removedInput schema / properties / domain_id / title
        Removed value: -"Domain Id"
      • addedInput schema / properties / lookback_s
        Added value: +{
        +  "default": 300,
        +  "description": "Window in seconds, 1..86400, default 300. A larger window may hit the 200-event cap.",
        +  "maximum": 86400,
        +  "minimum": 1,
        +  "type": "integer"
        +}
      • removedInput schema / properties / lookback_seconds
        Removed value: -{
        -  "default": 300,
        -  "description": "Window (in seconds) over which to return events. Defaults to 300 (5 minutes). Range: 1..86400 (1 second to 24 hours). Larger windows may hit the 200-event cap: narrow the window or filter on `domain_id` when that happens.",
        -  "maximum": 86400,
        -  "minimum": 1,
        -  "title": "Lookback Seconds",
        -  "type": "integer"
        -}
      • removedInput schema / title
        Removed value: -"participant_eventsArguments"
      • addedOutput schema / $defs / NameLifecycle
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "departures": {
        +      "description": "Times a participant with this name was lost.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "lives": {
        +      "description": "Distinct GUIDs that carried this name; restarts = `lives` - 1.",
        +      "minimum": 1,
        +      "type": "integer"
        +    }
        +  },
        +  "required": [
        +    "lives",
        +    "departures"
        +  ],
        +  "type": "object"
        +}
      • removedOutput schema / $defs / ParticipantEvent / description
        Removed value: -"A point-in-time lifecycle event (discovered or lost) for a DDS participant.\n\n`ParticipantInfo` is the current state; this is one event in its history.\nReturned by `participant_events`."
      • addedOutput schema / $defs / ParticipantEvent / properties / age_s
        Added value: +{
        +  "anyOf": [
        +    {
        +      "minimum": 0,
        +      "type": "number"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Seconds between `timestamp_ns` and this response; `None` in mock mode."
        +}
      • removedOutput schema / $defs / ParticipantEvent / properties / domain_id / description
        Removed value: -"DDS domain id the event occurred on."
      • removedOutput schema / $defs / ParticipantEvent / properties / domain_id / title
        Removed value: -"Domain Id"
      • changedOutput schema / $defs / ParticipantEvent / properties / event_type / description
        Previous value: -"`discovered` when the participant first joined the bus (or rejoined after leaving). `lost` when TopicForge observed a `REMOVED` callback (Fast DDS) or a polling delta where the GUID no longer appears in the DCPSParticipant builtin reader snapshot (Cyclone). The transition is reported once per state change."New value: +"`discovered` (joined or rejoined) or `lost` (removed, or gone from discovery); reported once per change."
      • removedOutput schema / $defs / ParticipantEvent / properties / event_type / title
        Removed value: -"Event Type"
      • removedOutput schema / $defs / ParticipantEvent / properties / guid / description
        Removed value: -"DDS Global Unique Identifier of the participant involved."
      • removedOutput schema / $defs / ParticipantEvent / properties / guid / title
        Removed value: -"Guid"
      • removedOutput schema / $defs / ParticipantEvent / properties / hostname / description
        Removed value: -"Hostname announced by the participant when known, else `None`."
      • removedOutput schema / $defs / ParticipantEvent / properties / hostname / title
        Removed value: -"Hostname"
      • removedOutput schema / $defs / ParticipantEvent / properties / mode_effective
        Removed value: -{
        -  "description": "Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`.",
        -  "enum": [
        -    "mock",
        -    "live"
        -  ],
        -  "title": "Mode Effective",
        -  "type": "string"
        -}
      • removedOutput schema / $defs / ParticipantEvent / properties / name / description
        Removed value: -"Participant name (EntityName QoS) when known, else `None`."
      • removedOutput schema / $defs / ParticipantEvent / properties / name / title
        Removed value: -"Name"
      • changedOutput schema / $defs / ParticipantEvent / properties / observed_ns / description
        Previous value: -"Local wall-clock time (ns since epoch) at which TopicForge noticed the event. Always at or after `timestamp_ns` when the latter is DDS-derived. `None` when not tracked."New value: +"Local ns when TopicForge noticed the event, never before a DDS-derived `timestamp_ns`; `None` when untracked."
      • removedOutput schema / $defs / ParticipantEvent / properties / observed_ns / title
        Removed value: -"Observed Ns"
      • changedOutput schema / $defs / ParticipantEvent / properties / time_source / description
        Previous value: -"Where `timestamp_ns` comes from. `dds_source_timestamp`: the DDS timestamp of the discovery announcement or dispose. For a `lost` event it is an upper bound: exact after a clean shutdown, the lease expiry after a crash (the process died up to one lease earlier: 10 s Cyclone default, 20 s Fast, 100 s RTI), and the two cannot be told apart. `observed_local`: the moment TopicForge noticed (weakest)."New value: +"`dds_source_timestamp` (DDS timestamp of the announcement or dispose; for `lost` an upper bound of the death) or `observed_local` (when TopicForge noticed, weakest)."
      • removedOutput schema / $defs / ParticipantEvent / properties / time_source / title
        Removed value: -"Time Source"
      • changedOutput schema / $defs / ParticipantEvent / properties / timestamp_ns / description
        Previous value: -"Wall-clock timestamp (nanoseconds since epoch) when TopicForge captured the event. For Fast DDS this is when the listener callback ran; for Cyclone this is when the polling delta was computed; for mock fixtures this is a deterministic anchor."New value: +"Wall-clock ns of the event; its origin is `time_source`."
      • removedOutput schema / $defs / ParticipantEvent / properties / timestamp_ns / title
        Removed value: -"Timestamp Ns"
      • removedOutput schema / $defs / ParticipantEvent / properties / vendor / description
        Removed value: -"DDS implementation tag for the participant involved, decoded the same way as `ParticipantInfo.vendor`."
      • removedOutput schema / $defs / ParticipantEvent / properties / vendor / title
        Removed value: -"Vendor"
      • changedOutput schema / $defs / ParticipantEvent / required
        Previous value: -[
        -  "guid",
        -  "event_type",
        -  "vendor",
        -  "timestamp_ns",
        -  "domain_id",
        -  "mode_effective"
        -]New value: +[
        +  "guid",
        +  "event_type",
        +  "vendor",
        +  "timestamp_ns",
        +  "domain_id"
        +]
      • removedOutput schema / $defs / ParticipantEvent / title
        Removed value: -"ParticipantEvent"
      • addedOutput schema / additionalProperties
        Added value: +false
      • addedOutput schema / description
        Added value: +"Envelope returned by `participant_events`."
      • addedOutput schema / properties / domain_id
        Added value: +{
        +  "description": "DDS domain observed (joined at startup).",
        +  "maximum": 232,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / events
        Added value: +{
        +  "items": {
        +    "$ref": "#/$defs/ParticipantEvent"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / mode_effective
        Added value: +{
        +  "description": "`live` or `mock` (fixtures).",
        +  "enum": [
        +    "mock",
        +    "live"
        +  ],
        +  "type": "string"
        +}
      • addedOutput schema / properties / note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One sentence of context, or `None`."
        +}
      • addedOutput schema / properties / restarts_by_name
        Added value: +{
        +  "additionalProperties": {
        +    "$ref": "#/$defs/NameLifecycle"
        +  },
        +  "description": "Names seen with several GUIDs or lost at least once (absent: ran once and stayed). A restart is a new GUID with the same name. Window only.",
        +  "type": "object"
        +}
      • addedOutput schema / properties / restarts_note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Set when unnamed participants were left out of `restarts_by_name`."
        +}
      • removedOutput schema / properties / result
        Removed value: -{
        -  "items": {
        -    "$ref": "#/$defs/ParticipantEvent"
        -  },
        -  "title": "Result",
        -  "type": "array"
        -}
      • addedOutput schema / properties / returned
        Added value: +{
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / total
        Added value: +{
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / truncated
        Added value: +{
        +  "description": "True when cut to the 200-event cap.",
        +  "type": "boolean"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "result"
        -]New value: +[
        +  "events",
        +  "returned",
        +  "total",
        +  "truncated",
        +  "domain_id",
        +  "mode_effective"
        +]
      • changedOutput schema / title
        Previous value: -"participant_eventsOutput"New value: +"ParticipantEventListing"
    • Changedpeek_bag_samples51 fields changed
      • changedInput schema / properties / count / description
        Previous value: -"Maximum number of recent messages to return. Defaults to 5; silently clamped to 50 (the hard cap that keeps tool output bounded; read it from `health_check.max_sample_count`). Negative values raise an error. The returned `SampleResult.count` reflects the actual number of samples produced: it can be lower than the request (empty topic, timeout, mock fixture shorter than requested)."New value: +"Messages to return, default 5, at most 50 (`health_check.max_sample_count`): more is capped to 50, negative raises an error. The result's `count` may be lower."
      • removedInput schema / properties / count / title
        Removed value: -"Count"
      • changedInput schema / properties / path / description
        Previous value: -"Path to a bag: a file ending in `.mcap` or `.db3`, or a `rosbag2_*` directory. `peek_bag_samples` also reads ROS 1 `.bag` files; `analyze_bag` does not (`ros2 bag info` cannot open them). Leading/trailing whitespace is stripped. Null bytes and otherwise malformed filesystem paths are rejected. Existence and bag format are validated by the live adapter (mock mode accepts any well-formed path)."New value: +"Bag path: a `.mcap` or `.db3` file or a `rosbag2_*` directory (`peek_bag_samples` also reads ROS 1 `.bag`; `analyze_bag` does not). Existence and format are checked live; mock accepts any well-formed path."
      • removedInput schema / properties / path / title
        Removed value: -"Path"
      • changedInput schema / properties / topic / description
        Previous value: -"Fully qualified ROS2 topic name starting with `/`, e.g. `/cmd_vel` or `/camera/image_raw`. Each `/`-separated segment must start with a letter or underscore and contain only letters, digits, and underscores; everything else (whitespace, quotes, shell metacharacters, `//`, trailing `/`) is rejected before reaching the `ros2` CLI."New value: +"Fully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI."
      • removedInput schema / properties / topic / title
        Removed value: -"Topic"
      • removedInput schema / title
        Removed value: -"peek_bag_samplesArguments"
      • addedOutput schema / $defs / ImageSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "data_length": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixel buffer size in bytes (see `data_length_basis`)."
        +    },
        +    "data_length_basis": {
        +      "anyOf": [
        +        {
        +          "enum": [
        +            "payload",
        +            "step_x_height"
        +          ],
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`payload` (counted) or `step_x_height` (array cut, so step times height)."
        +    },
        +    "encoding": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixel encoding, e.g. `rgb8`."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the camera."
        +    },
        +    "height": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixels."
        +    },
        +    "is_bigendian": {
        +      "anyOf": [
        +        {
        +          "type": "boolean"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Big-endian pixels."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when partial; else `null`."
        +    },
        +    "step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Row length, bytes."
        +    },
        +    "summary_type": {
        +      "const": "image",
        +      "type": "string"
        +    },
        +    "width": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixels."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "width",
        +    "height",
        +    "encoding",
        +    "step",
        +    "is_bigendian",
        +    "data_length",
        +    "data_length_basis"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ImuSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angular_velocity_norm": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `angular_velocity`, rad/s."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the IMU."
        +    },
        +    "linear_acceleration_norm": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `linear_acceleration`, m/s^2."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when a value is unusable; else `null`."
        +    },
        +    "orientation_valid": {
        +      "description": "False when the IMU gives no orientation: `roll`, `pitch`, `yaw` are `null`.",
        +      "type": "boolean"
        +    },
        +    "pitch": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about y, radians."
        +    },
        +    "roll": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about x, radians."
        +    },
        +    "summary_type": {
        +      "const": "imu",
        +      "type": "string"
        +    },
        +    "yaw": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about z, radians."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "orientation_valid",
        +    "roll",
        +    "pitch",
        +    "yaw",
        +    "angular_velocity_norm",
        +    "linear_acceleration_norm"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / LaserScanSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angle_increment": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Angle between beams, rad."
        +    },
        +    "angle_max": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bearing of the last beam, rad."
        +    },
        +    "angle_min": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bearing of beam 0, rad."
        +    },
        +    "beam_count": {
        +      "description": "Beams in `ranges`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "closest_obstacle": {
        +      "anyOf": [
        +        {
        +          "$ref": "#/$defs/ScanReturn"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Closest valid return of the scan; `null` with `note`."
        +    },
        +    "finite_count": {
        +      "description": "Beams with a finite range.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id`; every angle is in this sensor frame, not the base frame."
        +    },
        +    "inf_count": {
        +      "description": "Beams at `+inf`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "nan_count": {
        +      "description": "Beams at `nan`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "neg_inf_count": {
        +      "description": "Beams at `-inf`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when the summary is partial; else `null`."
        +    },
        +    "out_of_range_count": {
        +      "description": "Finite ranges outside `range_min`..`range_max` (not valid).",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "range_max": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Largest valid range, m."
        +    },
        +    "range_min": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Smallest valid range, m."
        +    },
        +    "sectors": {
        +      "$ref": "#/$defs/ScanSectors",
        +      "description": "Closest valid return per sector."
        +    },
        +    "summary_type": {
        +      "const": "laser_scan",
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "beam_count",
        +    "angle_min",
        +    "angle_max",
        +    "angle_increment",
        +    "range_min",
        +    "range_max",
        +    "finite_count",
        +    "inf_count",
        +    "neg_inf_count",
        +    "nan_count",
        +    "out_of_range_count",
        +    "closest_obstacle",
        +    "sectors"
        +  ],
        +  "type": "object"
        +}
      • removedOutput schema / $defs / MessageSample / description
        Removed value: -"A single sampled message on a topic."
      • removedOutput schema / $defs / MessageSample / properties / message_type / description
        Removed value: -"ROS2 message type of this sample, e.g. `geometry_msgs/msg/Twist`."
      • removedOutput schema / $defs / MessageSample / properties / message_type / title
        Removed value: -"Message Type"
      • changedOutput schema / $defs / MessageSample / properties / payload / description
        Previous value: -"Message fields as nested named values, e.g. `{\"header\": {\"stamp\": {\"sec\": 12, \"nanosec\": 5}, \"frame_id\": \"base_laser\"}, \"ranges\": [1.0, 1.001]}`. Non-finite floats are the strings `nan`, `inf`, `-inf`. Live `sample_messages` adds `_truncated_fields` (dotted paths of arrays, strings or bytes cut at `max_array_length`; a string of exactly that length plus `...` that was not cut is listed too) when something was cut, and `_raw_text` (the unparsed output) only when a message could not be parsed. With `arrays_summary_only` an array is a string like `<sequence type: float, length: 541>`. Large messages may be dropped to keep tool output bounded."New value: +"Message fields as nested values; `nan`/`inf` are strings. Live `sample_messages` adds `_truncated_fields` (cut paths) and `_raw_text` (unparsable output)."
      • removedOutput schema / $defs / MessageSample / properties / payload / title
        Removed value: -"Payload"
      • changedOutput schema / $defs / MessageSample / properties / received_ns / description
        Previous value: -"Wall-clock nanoseconds since epoch at which the `ros2` CLI printed this message, after it deserialized and formatted it: not the network receive time, and it lags more for large messages. Accurate to a fraction of a second. Live `sample_messages` only; `None` otherwise."New value: +"Wall-clock ns when the `ros2` CLI printed the message (live `sample_messages` only)."
      • removedOutput schema / $defs / MessageSample / properties / received_ns / title
        Removed value: -"Received Ns"
      • changedOutput schema / $defs / MessageSample / properties / recorded_ns / description
        Previous value: -"Nanoseconds at which the recorder wrote this message into the bag (the bag's own record time, wall clock unless the recorder ran on simulated time). `peek_bag_samples` only; `None` for live samples, where `received_ns` plays that role."New value: +"Bag record time in ns (`peek_bag_samples` only)."
      • removedOutput schema / $defs / MessageSample / properties / recorded_ns / title
        Removed value: -"Recorded Ns"
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / anyOf
        Removed value: -[
        -  {
        -    "enum": [
        -      "header",
        -      "none",
        -      "recorded"
        -    ],
        -    "type": "string"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / default
        Removed value: -null
      • changedOutput schema / $defs / MessageSample / properties / stamp_source / description
        Previous value: -"Where `timestamp_ns` comes from: `header` (the message's `header.stamp`), `none` (headerless live message, `timestamp_ns` is 0) or `recorded` (headerless message from a bag, `timestamp_ns` is the bag record time). `None` when the backend does not say."New value: +"Origin of `timestamp_ns`: `header`, `payload`, `recorded` (bag record time), `dds_source` or `none` (0)."
      • addedOutput schema / $defs / MessageSample / properties / stamp_source / enum
        Added value: +[
        +  "header",
        +  "payload",
        +  "recorded",
        +  "dds_source",
        +  "none"
        +]
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / title
        Removed value: -"Stamp Source"
      • addedOutput schema / $defs / MessageSample / properties / stamp_source / type
        Added value: +"string"
      • addedOutput schema / $defs / MessageSample / properties / summary
        Added value: +{
        +  "anyOf": [
        +    {
        +      "discriminator": {
        +        "mapping": {
        +          "image": "#/$defs/ImageSummary",
        +          "imu": "#/$defs/ImuSummary",
        +          "laser_scan": "#/$defs/LaserScanSummary",
        +          "odometry": "#/$defs/OdometrySummary",
        +          "point_cloud2": "#/$defs/PointCloud2Summary"
        +        },
        +        "propertyName": "summary_type"
        +      },
        +      "oneOf": [
        +        {
        +          "$ref": "#/$defs/LaserScanSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/OdometrySummary"
        +        },
        +        {
        +          "$ref": "#/$defs/ImuSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/ImageSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/PointCloud2Summary"
        +        }
        +      ]
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Condensed reading of the whole message (see `summary_type`); `null` for other types and DDS peeks."
        +}
      • changedOutput schema / $defs / MessageSample / properties / timestamp_ns / description
        Previous value: -"Nanoseconds from the message's own top-level `header.stamp` (`stamp_source` `header`), or 0 when the message has no header (`stamp_source` `none`, e.g. `std_msgs/String`, `geometry_msgs/Twist`). It is the publisher's clock, not the arrival time: on simulated time it is sim time since the simulation started (a few seconds, not a date) and 0 is a valid value. From `peek_bag_samples` a message without a header gets the bag record time instead (`stamp_source` `recorded`), also given in `recorded_ns`. Use `received_ns` for when the CLI printed the message. Mock samples with a header carry synthetic increasing values."New value: +"Publisher-clock ns: `header.stamp`, else the time in the body of `Clock`, `TFMessage` or `Log`, else 0 (see `stamp_source`); not the arrival time."
      • removedOutput schema / $defs / MessageSample / properties / timestamp_ns / title
        Removed value: -"Timestamp Ns"
      • removedOutput schema / $defs / MessageSample / properties / topic / description
        Removed value: -"Fully qualified topic name the sample was taken from, e.g. `/cmd_vel`."
      • removedOutput schema / $defs / MessageSample / properties / topic / title
        Removed value: -"Topic"
      • changedOutput schema / $defs / MessageSample / required
        Previous value: -[
        -  "topic",
        -  "message_type",
        -  "timestamp_ns"
        -]New value: +[
        +  "topic",
        +  "message_type",
        +  "timestamp_ns",
        +  "stamp_source"
        +]
      • removedOutput schema / $defs / MessageSample / title
        Removed value: -"MessageSample"
      • addedOutput schema / $defs / OdometrySummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angular_z": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`twist.twist.angular.z`, rad/s."
        +    },
        +    "child_frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`child_frame_id` (frame of the twist)."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` (frame of the pose)."
        +    },
        +    "linear_speed": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `twist.twist.linear`, m/s."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when a value is missing; else `null`."
        +    },
        +    "position_x": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.x`, m."
        +    },
        +    "position_y": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.y`, m."
        +    },
        +    "position_z": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.z`, m."
        +    },
        +    "summary_type": {
        +      "const": "odometry",
        +      "type": "string"
        +    },
        +    "yaw": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Heading from the pose quaternion, radians."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "child_frame_id",
        +    "linear_speed",
        +    "angular_z",
        +    "position_x",
        +    "position_y",
        +    "position_z",
        +    "yaw"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / PointCloud2Summary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "fields": {
        +      "description": "Point layout.",
        +      "items": {
        +        "$ref": "#/$defs/PointFieldSummary"
        +      },
        +      "type": "array"
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the cloud."
        +    },
        +    "height": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rows (1: unorganized)."
        +    },
        +    "is_dense": {
        +      "anyOf": [
        +        {
        +          "type": "boolean"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "True when no point is invalid."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when partial; else `null`."
        +    },
        +    "point_count": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`width` times `height`."
        +    },
        +    "point_step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bytes per point."
        +    },
        +    "row_step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bytes per row."
        +    },
        +    "summary_type": {
        +      "const": "point_cloud2",
        +      "type": "string"
        +    },
        +    "width": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Points per row."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "width",
        +    "height",
        +    "point_count",
        +    "point_step",
        +    "row_step",
        +    "is_dense",
        +    "fields"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / PointFieldSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "count": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Elements per point."
        +    },
        +    "datatype": {
        +      "description": "Element type, e.g. `FLOAT32`.",
        +      "type": "string"
        +    },
        +    "name": {
        +      "description": "Field name, e.g. `x`.",
        +      "type": "string"
        +    },
        +    "offset": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Byte offset in a point."
        +    }
        +  },
        +  "required": [
        +    "name",
        +    "datatype",
        +    "offset",
        +    "count"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanReturn
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "beam_index": {
        +      "description": "Index in `ranges`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "bearing": {
        +      "description": "Beam direction in radians, sensor frame: 0 is +x, counter-clockwise positive.",
        +      "type": "number"
        +    },
        +    "range": {
        +      "description": "Distance, meters.",
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "range",
        +    "bearing",
        +    "beam_index"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanSector
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "beam_count": {
        +      "description": "Beams inside the sector (0: not covered).",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "closest": {
        +      "anyOf": [
        +        {
        +          "$ref": "#/$defs/ScanReturn"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Closest valid return in the sector; `null` with `note`."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "Why `closest` is `null`; `null` when it is set."
        +    }
        +  },
        +  "required": [
        +    "beam_count",
        +    "closest"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanSectors
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "front": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings within +-45 degrees of +x."
        +    },
        +    "left": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings from 45 to 135 degrees."
        +    },
        +    "rear": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings beyond +-135 degrees."
        +    },
        +    "right": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings from -135 to -45 degrees."
        +    }
        +  },
        +  "required": [
        +    "front",
        +    "left",
        +    "right",
        +    "rear"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / TopicRate
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "basis": {
        +      "description": "Clock the rate is measured on: `received_ns` (live) or `recorded_ns` (bag).",
        +      "enum": [
        +        "received_ns",
        +        "recorded_ns"
        +      ],
        +      "type": "string"
        +    },
        +    "interval_cv": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Interval standard deviation over mean; `null` under 2 messages."
        +    },
        +    "interval_median_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Median interval; `null` under 2 messages."
        +    },
        +    "max_gap_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Longest interval; `null` under 2 messages."
        +    },
        +    "mean_interval_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Mean interval; `null` under 2 messages."
        +    },
        +    "message_count": {
        +      "description": "Messages the rate is computed from.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "observed_frequency_hz": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Messages per second on `basis`, drain excluded; the rate the caller received. `null` under 2 messages."
        +    },
        +    "sim_frequency_hz": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Same rate on the messages' own stamps (publisher clock); `null` without advancing stamps."
        +    },
        +    "startup_burst_count": {
        +      "description": "Leading messages set aside as a start-up drain (live only); still in `message_count`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "trailing_gap_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Time from the last message to the end of a live observation; else `null`."
        +    },
        +    "verdict": {
        +      "description": "First match: `silent` (none), `insufficient` (under 5), `intermittent` (an interval over 3x the median), `stable` (cv < 0.2), `jittery` (< 0.5), `erratic`. Describes this window, not a diagnosis.",
        +      "enum": [
        +        "silent",
        +        "insufficient",
        +        "intermittent",
        +        "stable",
        +        "jittery",
        +        "erratic"
        +      ],
        +      "type": "string"
        +    },
        +    "verdict_note": {
        +      "description": "The verdict in plain words.",
        +      "type": "string"
        +    },
        +    "window_s": {
        +      "description": "Observation length, seconds.",
        +      "minimum": 0,
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "basis",
        +    "message_count",
        +    "startup_burst_count",
        +    "window_s",
        +    "mean_interval_s",
        +    "interval_median_s",
        +    "max_gap_s",
        +    "interval_cv",
        +    "observed_frequency_hz",
        +    "sim_frequency_hz",
        +    "trailing_gap_s",
        +    "verdict",
        +    "verdict_note"
        +  ],
        +  "type": "object"
        +}
      • changedOutput schema / description
        Previous value: -"Envelope returned by the `sample_messages` tool."New value: +"Samples returned by `sample_messages`, `peek_dds_samples` and `peek_bag_samples`."
      • changedOutput schema / properties / count / description
        Previous value: -"Number of samples actually returned. May be 0 (no publisher active in live mode, or empty mock fixture), less than the requested count (topic yielded fewer messages within the timeout), or capped by the the silent maximum of 50: request `count > 50` and you will receive at most 50 without warning."New value: +"Number of samples in `samples`: fewer than requested when the topic gave fewer, never more than 50 (a larger request is capped to 50)."
      • removedOutput schema / properties / count / title
        Removed value: -"Count"
      • changedOutput schema / properties / mode_effective / description
        Previous value: -"Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`."New value: +"`live` or `mock` (fixtures)."
      • removedOutput schema / properties / mode_effective / title
        Removed value: -"Mode Effective"
      • changedOutput schema / properties / note / description
        Previous value: -"Why `samples` is empty or limited, when the cause is not obvious (for example payload decoding is disabled for DDS user topics). `None` when there is nothing to add."New value: +"Why `samples` is empty or limited, when not obvious; else `None`."
      • removedOutput schema / properties / note / title
        Removed value: -"Note"
      • addedOutput schema / properties / rate
        Added value: +{
        +  "anyOf": [
        +    {
        +      "$ref": "#/$defs/TopicRate"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "How the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10."
        +}
      • changedOutput schema / properties / samples / description
        Previous value: -"The sampled messages, ordered as received from the backend."New value: +"The samples, in backend order."
      • removedOutput schema / properties / samples / title
        Removed value: -"Samples"
      • removedOutput schema / properties / topic / description
        Removed value: -"Topic the samples were taken from, echoed from the request."
      • removedOutput schema / properties / topic / title
        Removed value: -"Topic"
    • Changedpeek_dds_samples49 fields changed
      • changedInput schema / properties / count / description
        Previous value: -"Maximum number of recent messages to return. Defaults to 5; silently clamped to 50 (the hard cap that keeps tool output bounded; read it from `health_check.max_sample_count`). Negative values raise an error. The returned `SampleResult.count` reflects the actual number of samples produced: it can be lower than the request (empty topic, timeout, mock fixture shorter than requested)."New value: +"Messages to return, default 5, at most 50 (`health_check.max_sample_count`): more is capped to 50, negative raises an error. The result's `count` may be lower."
      • removedInput schema / properties / count / title
        Removed value: -"Count"
      • changedInput schema / properties / topic / description
        Previous value: -"DDS topic name. Bare DDS names such as `scan` are valid, as are ROS 2 mangled names such as `rt/scan` and the builtin discovery topics `DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`. Letters, digits, `_`, `/` and `::` are allowed; anything else is rejected."New value: +"DDS topic name: bare (`scan`), ROS 2 mangled (`rt/scan`) or a builtin discovery topic (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Letters, digits, `_`, `/` and `::` only."
      • removedInput schema / properties / topic / title
        Removed value: -"Topic"
      • removedInput schema / title
        Removed value: -"peek_dds_samplesArguments"
      • addedOutput schema / $defs / ImageSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "data_length": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixel buffer size in bytes (see `data_length_basis`)."
        +    },
        +    "data_length_basis": {
        +      "anyOf": [
        +        {
        +          "enum": [
        +            "payload",
        +            "step_x_height"
        +          ],
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`payload` (counted) or `step_x_height` (array cut, so step times height)."
        +    },
        +    "encoding": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixel encoding, e.g. `rgb8`."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the camera."
        +    },
        +    "height": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixels."
        +    },
        +    "is_bigendian": {
        +      "anyOf": [
        +        {
        +          "type": "boolean"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Big-endian pixels."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when partial; else `null`."
        +    },
        +    "step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Row length, bytes."
        +    },
        +    "summary_type": {
        +      "const": "image",
        +      "type": "string"
        +    },
        +    "width": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixels."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "width",
        +    "height",
        +    "encoding",
        +    "step",
        +    "is_bigendian",
        +    "data_length",
        +    "data_length_basis"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ImuSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angular_velocity_norm": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `angular_velocity`, rad/s."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the IMU."
        +    },
        +    "linear_acceleration_norm": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `linear_acceleration`, m/s^2."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when a value is unusable; else `null`."
        +    },
        +    "orientation_valid": {
        +      "description": "False when the IMU gives no orientation: `roll`, `pitch`, `yaw` are `null`.",
        +      "type": "boolean"
        +    },
        +    "pitch": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about y, radians."
        +    },
        +    "roll": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about x, radians."
        +    },
        +    "summary_type": {
        +      "const": "imu",
        +      "type": "string"
        +    },
        +    "yaw": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about z, radians."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "orientation_valid",
        +    "roll",
        +    "pitch",
        +    "yaw",
        +    "angular_velocity_norm",
        +    "linear_acceleration_norm"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / LaserScanSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angle_increment": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Angle between beams, rad."
        +    },
        +    "angle_max": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bearing of the last beam, rad."
        +    },
        +    "angle_min": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bearing of beam 0, rad."
        +    },
        +    "beam_count": {
        +      "description": "Beams in `ranges`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "closest_obstacle": {
        +      "anyOf": [
        +        {
        +          "$ref": "#/$defs/ScanReturn"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Closest valid return of the scan; `null` with `note`."
        +    },
        +    "finite_count": {
        +      "description": "Beams with a finite range.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id`; every angle is in this sensor frame, not the base frame."
        +    },
        +    "inf_count": {
        +      "description": "Beams at `+inf`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "nan_count": {
        +      "description": "Beams at `nan`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "neg_inf_count": {
        +      "description": "Beams at `-inf`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when the summary is partial; else `null`."
        +    },
        +    "out_of_range_count": {
        +      "description": "Finite ranges outside `range_min`..`range_max` (not valid).",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "range_max": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Largest valid range, m."
        +    },
        +    "range_min": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Smallest valid range, m."
        +    },
        +    "sectors": {
        +      "$ref": "#/$defs/ScanSectors",
        +      "description": "Closest valid return per sector."
        +    },
        +    "summary_type": {
        +      "const": "laser_scan",
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "beam_count",
        +    "angle_min",
        +    "angle_max",
        +    "angle_increment",
        +    "range_min",
        +    "range_max",
        +    "finite_count",
        +    "inf_count",
        +    "neg_inf_count",
        +    "nan_count",
        +    "out_of_range_count",
        +    "closest_obstacle",
        +    "sectors"
        +  ],
        +  "type": "object"
        +}
      • removedOutput schema / $defs / MessageSample / description
        Removed value: -"A single sampled message on a topic."
      • removedOutput schema / $defs / MessageSample / properties / message_type / description
        Removed value: -"ROS2 message type of this sample, e.g. `geometry_msgs/msg/Twist`."
      • removedOutput schema / $defs / MessageSample / properties / message_type / title
        Removed value: -"Message Type"
      • changedOutput schema / $defs / MessageSample / properties / payload / description
        Previous value: -"Message fields as nested named values, e.g. `{\"header\": {\"stamp\": {\"sec\": 12, \"nanosec\": 5}, \"frame_id\": \"base_laser\"}, \"ranges\": [1.0, 1.001]}`. Non-finite floats are the strings `nan`, `inf`, `-inf`. Live `sample_messages` adds `_truncated_fields` (dotted paths of arrays, strings or bytes cut at `max_array_length`; a string of exactly that length plus `...` that was not cut is listed too) when something was cut, and `_raw_text` (the unparsed output) only when a message could not be parsed. With `arrays_summary_only` an array is a string like `<sequence type: float, length: 541>`. Large messages may be dropped to keep tool output bounded."New value: +"Message fields as nested values; `nan`/`inf` are strings. Live `sample_messages` adds `_truncated_fields` (cut paths) and `_raw_text` (unparsable output)."
      • removedOutput schema / $defs / MessageSample / properties / payload / title
        Removed value: -"Payload"
      • changedOutput schema / $defs / MessageSample / properties / received_ns / description
        Previous value: -"Wall-clock nanoseconds since epoch at which the `ros2` CLI printed this message, after it deserialized and formatted it: not the network receive time, and it lags more for large messages. Accurate to a fraction of a second. Live `sample_messages` only; `None` otherwise."New value: +"Wall-clock ns when the `ros2` CLI printed the message (live `sample_messages` only)."
      • removedOutput schema / $defs / MessageSample / properties / received_ns / title
        Removed value: -"Received Ns"
      • changedOutput schema / $defs / MessageSample / properties / recorded_ns / description
        Previous value: -"Nanoseconds at which the recorder wrote this message into the bag (the bag's own record time, wall clock unless the recorder ran on simulated time). `peek_bag_samples` only; `None` for live samples, where `received_ns` plays that role."New value: +"Bag record time in ns (`peek_bag_samples` only)."
      • removedOutput schema / $defs / MessageSample / properties / recorded_ns / title
        Removed value: -"Recorded Ns"
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / anyOf
        Removed value: -[
        -  {
        -    "enum": [
        -      "header",
        -      "none",
        -      "recorded"
        -    ],
        -    "type": "string"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / default
        Removed value: -null
      • changedOutput schema / $defs / MessageSample / properties / stamp_source / description
        Previous value: -"Where `timestamp_ns` comes from: `header` (the message's `header.stamp`), `none` (headerless live message, `timestamp_ns` is 0) or `recorded` (headerless message from a bag, `timestamp_ns` is the bag record time). `None` when the backend does not say."New value: +"Origin of `timestamp_ns`: `header`, `payload`, `recorded` (bag record time), `dds_source` or `none` (0)."
      • addedOutput schema / $defs / MessageSample / properties / stamp_source / enum
        Added value: +[
        +  "header",
        +  "payload",
        +  "recorded",
        +  "dds_source",
        +  "none"
        +]
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / title
        Removed value: -"Stamp Source"
      • addedOutput schema / $defs / MessageSample / properties / stamp_source / type
        Added value: +"string"
      • addedOutput schema / $defs / MessageSample / properties / summary
        Added value: +{
        +  "anyOf": [
        +    {
        +      "discriminator": {
        +        "mapping": {
        +          "image": "#/$defs/ImageSummary",
        +          "imu": "#/$defs/ImuSummary",
        +          "laser_scan": "#/$defs/LaserScanSummary",
        +          "odometry": "#/$defs/OdometrySummary",
        +          "point_cloud2": "#/$defs/PointCloud2Summary"
        +        },
        +        "propertyName": "summary_type"
        +      },
        +      "oneOf": [
        +        {
        +          "$ref": "#/$defs/LaserScanSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/OdometrySummary"
        +        },
        +        {
        +          "$ref": "#/$defs/ImuSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/ImageSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/PointCloud2Summary"
        +        }
        +      ]
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Condensed reading of the whole message (see `summary_type`); `null` for other types and DDS peeks."
        +}
      • changedOutput schema / $defs / MessageSample / properties / timestamp_ns / description
        Previous value: -"Nanoseconds from the message's own top-level `header.stamp` (`stamp_source` `header`), or 0 when the message has no header (`stamp_source` `none`, e.g. `std_msgs/String`, `geometry_msgs/Twist`). It is the publisher's clock, not the arrival time: on simulated time it is sim time since the simulation started (a few seconds, not a date) and 0 is a valid value. From `peek_bag_samples` a message without a header gets the bag record time instead (`stamp_source` `recorded`), also given in `recorded_ns`. Use `received_ns` for when the CLI printed the message. Mock samples with a header carry synthetic increasing values."New value: +"Publisher-clock ns: `header.stamp`, else the time in the body of `Clock`, `TFMessage` or `Log`, else 0 (see `stamp_source`); not the arrival time."
      • removedOutput schema / $defs / MessageSample / properties / timestamp_ns / title
        Removed value: -"Timestamp Ns"
      • removedOutput schema / $defs / MessageSample / properties / topic / description
        Removed value: -"Fully qualified topic name the sample was taken from, e.g. `/cmd_vel`."
      • removedOutput schema / $defs / MessageSample / properties / topic / title
        Removed value: -"Topic"
      • changedOutput schema / $defs / MessageSample / required
        Previous value: -[
        -  "topic",
        -  "message_type",
        -  "timestamp_ns"
        -]New value: +[
        +  "topic",
        +  "message_type",
        +  "timestamp_ns",
        +  "stamp_source"
        +]
      • removedOutput schema / $defs / MessageSample / title
        Removed value: -"MessageSample"
      • addedOutput schema / $defs / OdometrySummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angular_z": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`twist.twist.angular.z`, rad/s."
        +    },
        +    "child_frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`child_frame_id` (frame of the twist)."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` (frame of the pose)."
        +    },
        +    "linear_speed": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `twist.twist.linear`, m/s."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when a value is missing; else `null`."
        +    },
        +    "position_x": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.x`, m."
        +    },
        +    "position_y": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.y`, m."
        +    },
        +    "position_z": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.z`, m."
        +    },
        +    "summary_type": {
        +      "const": "odometry",
        +      "type": "string"
        +    },
        +    "yaw": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Heading from the pose quaternion, radians."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "child_frame_id",
        +    "linear_speed",
        +    "angular_z",
        +    "position_x",
        +    "position_y",
        +    "position_z",
        +    "yaw"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / PointCloud2Summary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "fields": {
        +      "description": "Point layout.",
        +      "items": {
        +        "$ref": "#/$defs/PointFieldSummary"
        +      },
        +      "type": "array"
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the cloud."
        +    },
        +    "height": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rows (1: unorganized)."
        +    },
        +    "is_dense": {
        +      "anyOf": [
        +        {
        +          "type": "boolean"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "True when no point is invalid."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when partial; else `null`."
        +    },
        +    "point_count": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`width` times `height`."
        +    },
        +    "point_step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bytes per point."
        +    },
        +    "row_step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bytes per row."
        +    },
        +    "summary_type": {
        +      "const": "point_cloud2",
        +      "type": "string"
        +    },
        +    "width": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Points per row."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "width",
        +    "height",
        +    "point_count",
        +    "point_step",
        +    "row_step",
        +    "is_dense",
        +    "fields"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / PointFieldSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "count": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Elements per point."
        +    },
        +    "datatype": {
        +      "description": "Element type, e.g. `FLOAT32`.",
        +      "type": "string"
        +    },
        +    "name": {
        +      "description": "Field name, e.g. `x`.",
        +      "type": "string"
        +    },
        +    "offset": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Byte offset in a point."
        +    }
        +  },
        +  "required": [
        +    "name",
        +    "datatype",
        +    "offset",
        +    "count"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanReturn
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "beam_index": {
        +      "description": "Index in `ranges`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "bearing": {
        +      "description": "Beam direction in radians, sensor frame: 0 is +x, counter-clockwise positive.",
        +      "type": "number"
        +    },
        +    "range": {
        +      "description": "Distance, meters.",
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "range",
        +    "bearing",
        +    "beam_index"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanSector
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "beam_count": {
        +      "description": "Beams inside the sector (0: not covered).",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "closest": {
        +      "anyOf": [
        +        {
        +          "$ref": "#/$defs/ScanReturn"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Closest valid return in the sector; `null` with `note`."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "Why `closest` is `null`; `null` when it is set."
        +    }
        +  },
        +  "required": [
        +    "beam_count",
        +    "closest"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanSectors
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "front": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings within +-45 degrees of +x."
        +    },
        +    "left": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings from 45 to 135 degrees."
        +    },
        +    "rear": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings beyond +-135 degrees."
        +    },
        +    "right": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings from -135 to -45 degrees."
        +    }
        +  },
        +  "required": [
        +    "front",
        +    "left",
        +    "right",
        +    "rear"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / TopicRate
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "basis": {
        +      "description": "Clock the rate is measured on: `received_ns` (live) or `recorded_ns` (bag).",
        +      "enum": [
        +        "received_ns",
        +        "recorded_ns"
        +      ],
        +      "type": "string"
        +    },
        +    "interval_cv": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Interval standard deviation over mean; `null` under 2 messages."
        +    },
        +    "interval_median_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Median interval; `null` under 2 messages."
        +    },
        +    "max_gap_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Longest interval; `null` under 2 messages."
        +    },
        +    "mean_interval_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Mean interval; `null` under 2 messages."
        +    },
        +    "message_count": {
        +      "description": "Messages the rate is computed from.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "observed_frequency_hz": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Messages per second on `basis`, drain excluded; the rate the caller received. `null` under 2 messages."
        +    },
        +    "sim_frequency_hz": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Same rate on the messages' own stamps (publisher clock); `null` without advancing stamps."
        +    },
        +    "startup_burst_count": {
        +      "description": "Leading messages set aside as a start-up drain (live only); still in `message_count`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "trailing_gap_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Time from the last message to the end of a live observation; else `null`."
        +    },
        +    "verdict": {
        +      "description": "First match: `silent` (none), `insufficient` (under 5), `intermittent` (an interval over 3x the median), `stable` (cv < 0.2), `jittery` (< 0.5), `erratic`. Describes this window, not a diagnosis.",
        +      "enum": [
        +        "silent",
        +        "insufficient",
        +        "intermittent",
        +        "stable",
        +        "jittery",
        +        "erratic"
        +      ],
        +      "type": "string"
        +    },
        +    "verdict_note": {
        +      "description": "The verdict in plain words.",
        +      "type": "string"
        +    },
        +    "window_s": {
        +      "description": "Observation length, seconds.",
        +      "minimum": 0,
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "basis",
        +    "message_count",
        +    "startup_burst_count",
        +    "window_s",
        +    "mean_interval_s",
        +    "interval_median_s",
        +    "max_gap_s",
        +    "interval_cv",
        +    "observed_frequency_hz",
        +    "sim_frequency_hz",
        +    "trailing_gap_s",
        +    "verdict",
        +    "verdict_note"
        +  ],
        +  "type": "object"
        +}
      • changedOutput schema / description
        Previous value: -"Envelope returned by the `sample_messages` tool."New value: +"Samples returned by `sample_messages`, `peek_dds_samples` and `peek_bag_samples`."
      • changedOutput schema / properties / count / description
        Previous value: -"Number of samples actually returned. May be 0 (no publisher active in live mode, or empty mock fixture), less than the requested count (topic yielded fewer messages within the timeout), or capped by the the silent maximum of 50: request `count > 50` and you will receive at most 50 without warning."New value: +"Number of samples in `samples`: fewer than requested when the topic gave fewer, never more than 50 (a larger request is capped to 50)."
      • removedOutput schema / properties / count / title
        Removed value: -"Count"
      • changedOutput schema / properties / mode_effective / description
        Previous value: -"Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`."New value: +"`live` or `mock` (fixtures)."
      • removedOutput schema / properties / mode_effective / title
        Removed value: -"Mode Effective"
      • changedOutput schema / properties / note / description
        Previous value: -"Why `samples` is empty or limited, when the cause is not obvious (for example payload decoding is disabled for DDS user topics). `None` when there is nothing to add."New value: +"Why `samples` is empty or limited, when not obvious; else `None`."
      • removedOutput schema / properties / note / title
        Removed value: -"Note"
      • addedOutput schema / properties / rate
        Added value: +{
        +  "anyOf": [
        +    {
        +      "$ref": "#/$defs/TopicRate"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "How the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10."
        +}
      • changedOutput schema / properties / samples / description
        Previous value: -"The sampled messages, ordered as received from the backend."New value: +"The samples, in backend order."
      • removedOutput schema / properties / samples / title
        Removed value: -"Samples"
      • removedOutput schema / properties / topic / description
        Removed value: -"Topic the samples were taken from, echoed from the request."
      • removedOutput schema / properties / topic / title
        Removed value: -"Topic"
    • Changedsample_messages56 fields changed
      • changedInput schema / properties / arrays_summary_only / description
        Previous value: -"When true, array fields are replaced by a short type and length summary instead of their elements. Use it to inspect the non-array fields of large messages (images, scans, point clouds). Defaults to false."New value: +"When true, arrays become a type-and-length summary: use it to read the non-array fields of large messages. Default false."
      • removedInput schema / properties / arrays_summary_only / title
        Removed value: -"Arrays Summary Only"
      • changedInput schema / properties / count / description
        Previous value: -"Number of messages to wait for. Defaults to 5; at most 50 (a larger request is capped to 50 and `note` says so). 0 returns nothing. `SampleResult.count` is how many arrived: fewer than requested when the topic publishes slowly or the deadline (`timeout_s`) ran out."New value: +"Messages to wait for, default 5, at most 50 (more is capped and `note` says so); 0 returns nothing. Fewer arrive when the topic is slow or `timeout_s` runs out."
      • removedInput schema / properties / count / title
        Removed value: -"Count"
      • changedInput schema / properties / max_array_length / description
        Previous value: -"Longest array, string or bytes value to return in full, 1..65536; longer ones are cut after their first N elements or characters and the field's dotted path is listed in the sample's `_truncated_fields`. Defaults to 128, the `ros2 topic echo` default, which cuts a 541-beam `LaserScan` after 128 ranges. Pass null to return everything in full (large for images and point clouds; a message over the server's size cap, 1 MiB by default, is dropped with a note, and a very large message may not print before the deadline)."New value: +"Longest array, string or bytes value returned in full, 1..65536, default 128 (a 541-beam `LaserScan` is cut to 128 ranges; its `summary` still covers all). Longer values are cut and listed in `_truncated_fields`. Null returns everything (large for images and clouds; a message over 1 MiB is dropped with a note)."
      • removedInput schema / properties / max_array_length / title
        Removed value: -"Max Array Length"
      • changedInput schema / properties / timeout_s / description
        Previous value: -"Seconds to wait for the messages, 1..45, default 10. Bounds the whole call, topic lookup and the `ros2` CLI start-up (a few seconds on a slow machine) included: the call returns within about `timeout_s` + 2 s. Whatever arrived by then is returned with a `note`. Raise it for a topic that publishes slower than 1 Hz. The server handles one call at a time, so a long wait delays other tool calls."New value: +"Seconds to wait, 1..40, default 10, covering topic lookup and CLI start-up. The call returns within `timeout_s` plus a few seconds, with what arrived and a `note`. Raise it for a topic slower than 1 Hz."
      • changedInput schema / properties / timeout_s / maximum
        Previous value: -45New value: +40
      • removedInput schema / properties / timeout_s / title
        Removed value: -"Timeout S"
      • changedInput schema / properties / topic / description
        Previous value: -"Fully qualified ROS2 topic name starting with `/`, e.g. `/cmd_vel` or `/camera/image_raw`. Each `/`-separated segment must start with a letter or underscore and contain only letters, digits, and underscores; everything else (whitespace, quotes, shell metacharacters, `//`, trailing `/`) is rejected before reaching the `ros2` CLI."New value: +"Fully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI."
      • removedInput schema / properties / topic / title
        Removed value: -"Topic"
      • removedInput schema / title
        Removed value: -"sample_messagesArguments"
      • addedOutput schema / $defs / ImageSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "data_length": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixel buffer size in bytes (see `data_length_basis`)."
        +    },
        +    "data_length_basis": {
        +      "anyOf": [
        +        {
        +          "enum": [
        +            "payload",
        +            "step_x_height"
        +          ],
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`payload` (counted) or `step_x_height` (array cut, so step times height)."
        +    },
        +    "encoding": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixel encoding, e.g. `rgb8`."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the camera."
        +    },
        +    "height": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixels."
        +    },
        +    "is_bigendian": {
        +      "anyOf": [
        +        {
        +          "type": "boolean"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Big-endian pixels."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when partial; else `null`."
        +    },
        +    "step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Row length, bytes."
        +    },
        +    "summary_type": {
        +      "const": "image",
        +      "type": "string"
        +    },
        +    "width": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Pixels."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "width",
        +    "height",
        +    "encoding",
        +    "step",
        +    "is_bigendian",
        +    "data_length",
        +    "data_length_basis"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ImuSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angular_velocity_norm": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `angular_velocity`, rad/s."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the IMU."
        +    },
        +    "linear_acceleration_norm": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `linear_acceleration`, m/s^2."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when a value is unusable; else `null`."
        +    },
        +    "orientation_valid": {
        +      "description": "False when the IMU gives no orientation: `roll`, `pitch`, `yaw` are `null`.",
        +      "type": "boolean"
        +    },
        +    "pitch": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about y, radians."
        +    },
        +    "roll": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about x, radians."
        +    },
        +    "summary_type": {
        +      "const": "imu",
        +      "type": "string"
        +    },
        +    "yaw": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rotation about z, radians."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "orientation_valid",
        +    "roll",
        +    "pitch",
        +    "yaw",
        +    "angular_velocity_norm",
        +    "linear_acceleration_norm"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / LaserScanSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angle_increment": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Angle between beams, rad."
        +    },
        +    "angle_max": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bearing of the last beam, rad."
        +    },
        +    "angle_min": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bearing of beam 0, rad."
        +    },
        +    "beam_count": {
        +      "description": "Beams in `ranges`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "closest_obstacle": {
        +      "anyOf": [
        +        {
        +          "$ref": "#/$defs/ScanReturn"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Closest valid return of the scan; `null` with `note`."
        +    },
        +    "finite_count": {
        +      "description": "Beams with a finite range.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id`; every angle is in this sensor frame, not the base frame."
        +    },
        +    "inf_count": {
        +      "description": "Beams at `+inf`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "nan_count": {
        +      "description": "Beams at `nan`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "neg_inf_count": {
        +      "description": "Beams at `-inf`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when the summary is partial; else `null`."
        +    },
        +    "out_of_range_count": {
        +      "description": "Finite ranges outside `range_min`..`range_max` (not valid).",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "range_max": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Largest valid range, m."
        +    },
        +    "range_min": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Smallest valid range, m."
        +    },
        +    "sectors": {
        +      "$ref": "#/$defs/ScanSectors",
        +      "description": "Closest valid return per sector."
        +    },
        +    "summary_type": {
        +      "const": "laser_scan",
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "beam_count",
        +    "angle_min",
        +    "angle_max",
        +    "angle_increment",
        +    "range_min",
        +    "range_max",
        +    "finite_count",
        +    "inf_count",
        +    "neg_inf_count",
        +    "nan_count",
        +    "out_of_range_count",
        +    "closest_obstacle",
        +    "sectors"
        +  ],
        +  "type": "object"
        +}
      • removedOutput schema / $defs / MessageSample / description
        Removed value: -"A single sampled message on a topic."
      • removedOutput schema / $defs / MessageSample / properties / message_type / description
        Removed value: -"ROS2 message type of this sample, e.g. `geometry_msgs/msg/Twist`."
      • removedOutput schema / $defs / MessageSample / properties / message_type / title
        Removed value: -"Message Type"
      • changedOutput schema / $defs / MessageSample / properties / payload / description
        Previous value: -"Message fields as nested named values, e.g. `{\"header\": {\"stamp\": {\"sec\": 12, \"nanosec\": 5}, \"frame_id\": \"base_laser\"}, \"ranges\": [1.0, 1.001]}`. Non-finite floats are the strings `nan`, `inf`, `-inf`. Live `sample_messages` adds `_truncated_fields` (dotted paths of arrays, strings or bytes cut at `max_array_length`; a string of exactly that length plus `...` that was not cut is listed too) when something was cut, and `_raw_text` (the unparsed output) only when a message could not be parsed. With `arrays_summary_only` an array is a string like `<sequence type: float, length: 541>`. Large messages may be dropped to keep tool output bounded."New value: +"Message fields as nested values; `nan`/`inf` are strings. Live `sample_messages` adds `_truncated_fields` (cut paths) and `_raw_text` (unparsable output)."
      • removedOutput schema / $defs / MessageSample / properties / payload / title
        Removed value: -"Payload"
      • changedOutput schema / $defs / MessageSample / properties / received_ns / description
        Previous value: -"Wall-clock nanoseconds since epoch at which the `ros2` CLI printed this message, after it deserialized and formatted it: not the network receive time, and it lags more for large messages. Accurate to a fraction of a second. Live `sample_messages` only; `None` otherwise."New value: +"Wall-clock ns when the `ros2` CLI printed the message (live `sample_messages` only)."
      • removedOutput schema / $defs / MessageSample / properties / received_ns / title
        Removed value: -"Received Ns"
      • changedOutput schema / $defs / MessageSample / properties / recorded_ns / description
        Previous value: -"Nanoseconds at which the recorder wrote this message into the bag (the bag's own record time, wall clock unless the recorder ran on simulated time). `peek_bag_samples` only; `None` for live samples, where `received_ns` plays that role."New value: +"Bag record time in ns (`peek_bag_samples` only)."
      • removedOutput schema / $defs / MessageSample / properties / recorded_ns / title
        Removed value: -"Recorded Ns"
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / anyOf
        Removed value: -[
        -  {
        -    "enum": [
        -      "header",
        -      "none",
        -      "recorded"
        -    ],
        -    "type": "string"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / default
        Removed value: -null
      • changedOutput schema / $defs / MessageSample / properties / stamp_source / description
        Previous value: -"Where `timestamp_ns` comes from: `header` (the message's `header.stamp`), `none` (headerless live message, `timestamp_ns` is 0) or `recorded` (headerless message from a bag, `timestamp_ns` is the bag record time). `None` when the backend does not say."New value: +"Origin of `timestamp_ns`: `header`, `payload`, `recorded` (bag record time), `dds_source` or `none` (0)."
      • addedOutput schema / $defs / MessageSample / properties / stamp_source / enum
        Added value: +[
        +  "header",
        +  "payload",
        +  "recorded",
        +  "dds_source",
        +  "none"
        +]
      • removedOutput schema / $defs / MessageSample / properties / stamp_source / title
        Removed value: -"Stamp Source"
      • addedOutput schema / $defs / MessageSample / properties / stamp_source / type
        Added value: +"string"
      • addedOutput schema / $defs / MessageSample / properties / summary
        Added value: +{
        +  "anyOf": [
        +    {
        +      "discriminator": {
        +        "mapping": {
        +          "image": "#/$defs/ImageSummary",
        +          "imu": "#/$defs/ImuSummary",
        +          "laser_scan": "#/$defs/LaserScanSummary",
        +          "odometry": "#/$defs/OdometrySummary",
        +          "point_cloud2": "#/$defs/PointCloud2Summary"
        +        },
        +        "propertyName": "summary_type"
        +      },
        +      "oneOf": [
        +        {
        +          "$ref": "#/$defs/LaserScanSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/OdometrySummary"
        +        },
        +        {
        +          "$ref": "#/$defs/ImuSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/ImageSummary"
        +        },
        +        {
        +          "$ref": "#/$defs/PointCloud2Summary"
        +        }
        +      ]
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Condensed reading of the whole message (see `summary_type`); `null` for other types and DDS peeks."
        +}
      • changedOutput schema / $defs / MessageSample / properties / timestamp_ns / description
        Previous value: -"Nanoseconds from the message's own top-level `header.stamp` (`stamp_source` `header`), or 0 when the message has no header (`stamp_source` `none`, e.g. `std_msgs/String`, `geometry_msgs/Twist`). It is the publisher's clock, not the arrival time: on simulated time it is sim time since the simulation started (a few seconds, not a date) and 0 is a valid value. From `peek_bag_samples` a message without a header gets the bag record time instead (`stamp_source` `recorded`), also given in `recorded_ns`. Use `received_ns` for when the CLI printed the message. Mock samples with a header carry synthetic increasing values."New value: +"Publisher-clock ns: `header.stamp`, else the time in the body of `Clock`, `TFMessage` or `Log`, else 0 (see `stamp_source`); not the arrival time."
      • removedOutput schema / $defs / MessageSample / properties / timestamp_ns / title
        Removed value: -"Timestamp Ns"
      • removedOutput schema / $defs / MessageSample / properties / topic / description
        Removed value: -"Fully qualified topic name the sample was taken from, e.g. `/cmd_vel`."
      • removedOutput schema / $defs / MessageSample / properties / topic / title
        Removed value: -"Topic"
      • changedOutput schema / $defs / MessageSample / required
        Previous value: -[
        -  "topic",
        -  "message_type",
        -  "timestamp_ns"
        -]New value: +[
        +  "topic",
        +  "message_type",
        +  "timestamp_ns",
        +  "stamp_source"
        +]
      • removedOutput schema / $defs / MessageSample / title
        Removed value: -"MessageSample"
      • addedOutput schema / $defs / OdometrySummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "angular_z": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`twist.twist.angular.z`, rad/s."
        +    },
        +    "child_frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`child_frame_id` (frame of the twist)."
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` (frame of the pose)."
        +    },
        +    "linear_speed": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Norm of `twist.twist.linear`, m/s."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when a value is missing; else `null`."
        +    },
        +    "position_x": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.x`, m."
        +    },
        +    "position_y": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.y`, m."
        +    },
        +    "position_z": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`pose.pose.position.z`, m."
        +    },
        +    "summary_type": {
        +      "const": "odometry",
        +      "type": "string"
        +    },
        +    "yaw": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Heading from the pose quaternion, radians."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "child_frame_id",
        +    "linear_speed",
        +    "angular_z",
        +    "position_x",
        +    "position_y",
        +    "position_z",
        +    "yaw"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / PointCloud2Summary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "fields": {
        +      "description": "Point layout.",
        +      "items": {
        +        "$ref": "#/$defs/PointFieldSummary"
        +      },
        +      "type": "array"
        +    },
        +    "frame_id": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`header.frame_id` of the cloud."
        +    },
        +    "height": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Rows (1: unorganized)."
        +    },
        +    "is_dense": {
        +      "anyOf": [
        +        {
        +          "type": "boolean"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "True when no point is invalid."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "One sentence when partial; else `null`."
        +    },
        +    "point_count": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "`width` times `height`."
        +    },
        +    "point_step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bytes per point."
        +    },
        +    "row_step": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Bytes per row."
        +    },
        +    "summary_type": {
        +      "const": "point_cloud2",
        +      "type": "string"
        +    },
        +    "width": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Points per row."
        +    }
        +  },
        +  "required": [
        +    "summary_type",
        +    "frame_id",
        +    "width",
        +    "height",
        +    "point_count",
        +    "point_step",
        +    "row_step",
        +    "is_dense",
        +    "fields"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / PointFieldSummary
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "count": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Elements per point."
        +    },
        +    "datatype": {
        +      "description": "Element type, e.g. `FLOAT32`.",
        +      "type": "string"
        +    },
        +    "name": {
        +      "description": "Field name, e.g. `x`.",
        +      "type": "string"
        +    },
        +    "offset": {
        +      "anyOf": [
        +        {
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Byte offset in a point."
        +    }
        +  },
        +  "required": [
        +    "name",
        +    "datatype",
        +    "offset",
        +    "count"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanReturn
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "beam_index": {
        +      "description": "Index in `ranges`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "bearing": {
        +      "description": "Beam direction in radians, sensor frame: 0 is +x, counter-clockwise positive.",
        +      "type": "number"
        +    },
        +    "range": {
        +      "description": "Distance, meters.",
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "range",
        +    "bearing",
        +    "beam_index"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanSector
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "beam_count": {
        +      "description": "Beams inside the sector (0: not covered).",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "closest": {
        +      "anyOf": [
        +        {
        +          "$ref": "#/$defs/ScanReturn"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Closest valid return in the sector; `null` with `note`."
        +    },
        +    "note": {
        +      "anyOf": [
        +        {
        +          "type": "string"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "default": null,
        +      "description": "Why `closest` is `null`; `null` when it is set."
        +    }
        +  },
        +  "required": [
        +    "beam_count",
        +    "closest"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / ScanSectors
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "front": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings within +-45 degrees of +x."
        +    },
        +    "left": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings from 45 to 135 degrees."
        +    },
        +    "rear": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings beyond +-135 degrees."
        +    },
        +    "right": {
        +      "$ref": "#/$defs/ScanSector",
        +      "description": "Bearings from -135 to -45 degrees."
        +    }
        +  },
        +  "required": [
        +    "front",
        +    "left",
        +    "right",
        +    "rear"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / $defs / TopicRate
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "basis": {
        +      "description": "Clock the rate is measured on: `received_ns` (live) or `recorded_ns` (bag).",
        +      "enum": [
        +        "received_ns",
        +        "recorded_ns"
        +      ],
        +      "type": "string"
        +    },
        +    "interval_cv": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Interval standard deviation over mean; `null` under 2 messages."
        +    },
        +    "interval_median_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Median interval; `null` under 2 messages."
        +    },
        +    "max_gap_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Longest interval; `null` under 2 messages."
        +    },
        +    "mean_interval_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Mean interval; `null` under 2 messages."
        +    },
        +    "message_count": {
        +      "description": "Messages the rate is computed from.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "observed_frequency_hz": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Messages per second on `basis`, drain excluded; the rate the caller received. `null` under 2 messages."
        +    },
        +    "sim_frequency_hz": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Same rate on the messages' own stamps (publisher clock); `null` without advancing stamps."
        +    },
        +    "startup_burst_count": {
        +      "description": "Leading messages set aside as a start-up drain (live only); still in `message_count`.",
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "trailing_gap_s": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ],
        +      "description": "Time from the last message to the end of a live observation; else `null`."
        +    },
        +    "verdict": {
        +      "description": "First match: `silent` (none), `insufficient` (under 5), `intermittent` (an interval over 3x the median), `stable` (cv < 0.2), `jittery` (< 0.5), `erratic`. Describes this window, not a diagnosis.",
        +      "enum": [
        +        "silent",
        +        "insufficient",
        +        "intermittent",
        +        "stable",
        +        "jittery",
        +        "erratic"
        +      ],
        +      "type": "string"
        +    },
        +    "verdict_note": {
        +      "description": "The verdict in plain words.",
        +      "type": "string"
        +    },
        +    "window_s": {
        +      "description": "Observation length, seconds.",
        +      "minimum": 0,
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "basis",
        +    "message_count",
        +    "startup_burst_count",
        +    "window_s",
        +    "mean_interval_s",
        +    "interval_median_s",
        +    "max_gap_s",
        +    "interval_cv",
        +    "observed_frequency_hz",
        +    "sim_frequency_hz",
        +    "trailing_gap_s",
        +    "verdict",
        +    "verdict_note"
        +  ],
        +  "type": "object"
        +}
      • changedOutput schema / description
        Previous value: -"Envelope returned by the `sample_messages` tool."New value: +"Samples returned by `sample_messages`, `peek_dds_samples` and `peek_bag_samples`."
      • changedOutput schema / properties / count / description
        Previous value: -"Number of samples actually returned. May be 0 (no publisher active in live mode, or empty mock fixture), less than the requested count (topic yielded fewer messages within the timeout), or capped by the the silent maximum of 50: request `count > 50` and you will receive at most 50 without warning."New value: +"Number of samples in `samples`: fewer than requested when the topic gave fewer, never more than 50 (a larger request is capped to 50)."
      • removedOutput schema / properties / count / title
        Removed value: -"Count"
      • changedOutput schema / properties / mode_effective / description
        Previous value: -"Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`."New value: +"`live` or `mock` (fixtures)."
      • removedOutput schema / properties / mode_effective / title
        Removed value: -"Mode Effective"
      • changedOutput schema / properties / note / description
        Previous value: -"Why `samples` is empty or limited, when the cause is not obvious (for example payload decoding is disabled for DDS user topics). `None` when there is nothing to add."New value: +"Why `samples` is empty or limited, when not obvious; else `None`."
      • removedOutput schema / properties / note / title
        Removed value: -"Note"
      • addedOutput schema / properties / rate
        Added value: +{
        +  "anyOf": [
        +    {
        +      "$ref": "#/$defs/TopicRate"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "How the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10."
        +}
      • changedOutput schema / properties / samples / description
        Previous value: -"The sampled messages, ordered as received from the backend."New value: +"The samples, in backend order."
      • removedOutput schema / properties / samples / title
        Removed value: -"Samples"
      • removedOutput schema / properties / topic / description
        Removed value: -"Topic the samples were taken from, echoed from the request."
      • removedOutput schema / properties / topic / title
        Removed value: -"Topic"
    • Changedtopic_metrics36 fields changed
      • changedInput schema / properties / domain_id / description
        Previous value: -"Accepted for compatibility (0..232). TopicForge observes the domain it joined at startup (TOPICFORGE_DDS_DOMAIN_ID); this argument does not switch domains, and the response `domain_id` says which one was observed."New value: +"Ignored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` flags a different non-zero value."
      • removedInput schema / properties / domain_id / title
        Removed value: -"Domain Id"
      • changedInput schema / properties / topic / description
        Previous value: -"DDS topic name. Bare DDS names such as `scan` are valid, as are ROS 2 mangled names such as `rt/scan` and the builtin discovery topics `DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`. Letters, digits, `_`, `/` and `::` are allowed; anything else is rejected."New value: +"DDS topic name: bare (`scan`), ROS 2 mangled (`rt/scan`) or a builtin discovery topic (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Letters, digits, `_`, `/` and `::` only."
      • removedInput schema / properties / topic / title
        Removed value: -"Topic"
      • addedInput schema / properties / window_s
        Added value: +{
        +  "default": 60,
        +  "description": "Window in seconds, 1..3600, default 60.",
        +  "maximum": 3600,
        +  "minimum": 1,
        +  "type": "integer"
        +}
      • removedInput schema / properties / window_seconds
        Removed value: -{
        -  "default": 60,
        -  "description": "Window in seconds over which to compute metrics (1..3600). Defaults to 60 seconds. Smaller windows reflect more recent state; larger windows smooth transient anomalies.",
        -  "maximum": 3600,
        -  "minimum": 1,
        -  "title": "Window Seconds",
        -  "type": "integer"
        -}
      • removedInput schema / title
        Removed value: -"topic_metricsArguments"
      • changedOutput schema / description
        Previous value: -"Temporal metrics for a single DDS topic over a recent window.\n\nBuilt from samples that pass through `peek_dds_samples`. The buffer is\nfilled on demand because neither the `cyclonedds` nor the `fastdds`\nPython binding has a reliable per-sample callback, so a burst between two\ncalls is not seen.\n\nNumeric fields are `None` (`0` for `sequence_gaps_count`) when the data is\nmissing: no samples, no source timestamps, no sequence number in the\npayload. `samples_observed=0` is a valid result, not an error."New value: +"Temporal metrics for a single DDS topic over a recent window."
      • addedOutput schema / properties / declared_frequency_hz
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "number"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "`1 / deadline` of the shortest writer Deadline announced: the promised rate, not the observed one; `None` when none announced."
        +}
      • removedOutput schema / properties / frequency_hz_declared
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "type": "number"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "Declared, not measured: `1 / deadline` for the shortest QoS Deadline period announced by a writer on this topic in discovery. `None` when no writer announced a finite Deadline or the topic is not announced. It is the rate the application promised, not the rate observed.",
        -  "title": "Frequency Hz Declared"
        -}
      • removedOutput schema / properties / frequency_hz_observed
        Removed value: -{
        -  "anyOf": [
        -    {
        -      "type": "number"
        -    },
        -    {
        -      "type": "null"
        -    }
        -  ],
        -  "default": null,
        -  "description": "`samples_observed / window_seconds_actual`. `None` when fewer than 2 samples were observed (a single sample does not define a frequency).",
        -  "title": "Frequency Hz Observed"
        -}
      • changedOutput schema / properties / latency_available / description
        Previous value: -"True when at least one sample in the window carried both a publish timestamp and a receive timestamp. The percentile fields are `None` when this is False."New value: +"True when a sample carried both a publish and a receive timestamp."
      • removedOutput schema / properties / latency_available / title
        Removed value: -"Latency Available"
      • changedOutput schema / properties / latency_ns_p50 / description
        Previous value: -"Median publish-to-receive latency in nanoseconds, computed only when the sample type exposes a publish timestamp (typically via `header.stamp` on `Header`-stamped messages). `None` when `latency_available=False`."New value: +"Median publish-to-receive latency in ns; `None` unless `latency_available`."
      • removedOutput schema / properties / latency_ns_p50 / title
        Removed value: -"Latency Ns P50"
      • removedOutput schema / properties / latency_ns_p95 / title
        Removed value: -"Latency Ns P95"
      • removedOutput schema / properties / latency_ns_p99 / title
        Removed value: -"Latency Ns P99"
      • changedOutput schema / properties / mode_effective / description
        Previous value: -"Runtime mode the adapter served this response in: `live` (real ROS2 introspection) or `mock` (deterministic fixtures). Lets a caller tell a real graph from a demo one without calling `health_check`."New value: +"`live` or `mock` (fixtures)."
      • removedOutput schema / properties / mode_effective / title
        Removed value: -"Mode Effective"
      • addedOutput schema / properties / note
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "One sentence of context, or `None`."
        +}
      • addedOutput schema / properties / observed_frequency_hz
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "number"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "`samples_observed / window_actual_s`; `None` under 2 samples."
        +}
      • changedOutput schema / properties / samples_observed / description
        Previous value: -"Number of samples in the buffer matching `topic` within the window. `0` means TopicForge has not seen any sample on this topic recently: it does NOT mean the topic has no publisher, only that no `peek_dds_samples` call captured one in the window."New value: +"Samples buffered in the window. `0` does NOT mean no publisher, only that no `peek_dds_samples` call captured one."
      • removedOutput schema / properties / samples_observed / title
        Removed value: -"Samples Observed"
      • changedOutput schema / properties / sequence_gaps_count / description
        Previous value: -"Number of missing sequence numbers detected in the buffered samples. `0` either means no gaps observed OR the sample type did not expose a sequence number (check `sequence_numbers_available` to disambiguate)."New value: +"Missing sequence numbers in the buffered samples; `0` also when none exist (see `sequence_numbers_available`)."
      • removedOutput schema / properties / sequence_gaps_count / title
        Removed value: -"Sequence Gaps Count"
      • changedOutput schema / properties / sequence_numbers_available / description
        Previous value: -"True when the adapter successfully extracted sequence numbers from at least one sample. Sequence number support depends on the message type: `Header`-stamped messages with a `seq` field expose it; primitives like `std_msgs/String` do not."New value: +"True when a sequence number could be read from a sample."
      • removedOutput schema / properties / sequence_numbers_available / title
        Removed value: -"Sequence Numbers Available"
      • changedOutput schema / properties / status / description
        Previous value: -"How to read the numbers. `unsupported_user_topic`: the topic is a user topic, whose payload is not decoded, so no metric exists and the null fields are not a measurement. `no_samples_yet`: a supported topic with nothing buffered in the window. `ok`: metrics computed from buffered samples."New value: +"`ok`; `no_samples_yet` (empty buffer); `unsupported_user_topic` (payload not decoded: null fields are not measurements)."
      • removedOutput schema / properties / status / title
        Removed value: -"Status"
      • removedOutput schema / properties / topic / description
        Removed value: -"Topic the metrics were computed for."
      • removedOutput schema / properties / topic / title
        Removed value: -"Topic"
      • addedOutput schema / properties / window_actual_s
        Added value: +{
        +  "description": "Seconds actually covered; shorter than `window_s` after a recent start; `0.0` without samples.",
        +  "minimum": 0,
        +  "type": "number"
        +}
      • addedOutput schema / properties / window_s
        Added value: +{
        +  "description": "Requested window in seconds (echoed).",
        +  "maximum": 3600,
        +  "minimum": 1,
        +  "type": "integer"
        +}
      • removedOutput schema / properties / window_seconds
        Removed value: -{
        -  "description": "Requested window in seconds (1..3600). Echoed back from the tool call so the LLM can correlate the request.",
        -  "maximum": 3600,
        -  "minimum": 1,
        -  "title": "Window Seconds",
        -  "type": "integer"
        -}
      • removedOutput schema / properties / window_seconds_actual
        Removed value: -{
        -  "description": "Actual elapsed seconds within the window. May be smaller than `window_seconds` when the adapter buffered samples for less time than the requested window (e.g., the server just started). `0.0` when `samples_observed=0`.",
        -  "minimum": 0,
        -  "title": "Window Seconds Actual",
        -  "type": "number"
        -}
      • changedOutput schema / required
        Previous value: -[
        -  "topic",
        -  "window_seconds",
        -  "window_seconds_actual",
        -  "samples_observed",
        -  "mode_effective"
        -]New value: +[
        +  "topic",
        +  "window_s",
        +  "window_actual_s",
        +  "samples_observed",
        +  "mode_effective"
        +]
  2. 4 tool updatesv0.6.3
    • Changedhealth_check1 field changed
      • addedOutput schema / properties / sim_clock_published
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "boolean"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Whether `/clock` has at least one publisher on the ROS 2 graph. True suggests nodes may run on simulated time (`use_sim_time`), so `header.stamp` values are sim time, not wall time (compare with `received_ns`). It is a hint: a publisher on `/clock` does not prove a given node follows it. `None` in mock mode or when the `ros2` CLI could not answer.",
        +  "title": "Sim Clock Published"
        +}
    • Changedpeek_bag_samples5 fields changed
      • changedOutput schema / $defs / MessageSample / properties / payload / description
        Previous value: -"Structured message payload. **In live mode the parser exposes the message fields as positional CSV columns** keyed as `col_0`, `col_1`, ... (`header.stamp.sec`/`nanosec` are stripped out into `timestamp_ns` when detected). The raw CSV row is preserved verbatim under the reserved `_raw_text` key so clients can re-parse against the message's IDL when needed. In mock mode the payload is fully structured and `_raw_text` is absent. Large messages (e.g. images) may be summarized to keep tool output bounded."New value: +"Message fields as nested named values, e.g. `{\"header\": {\"stamp\": {\"sec\": 12, \"nanosec\": 5}, \"frame_id\": \"base_laser\"}, \"ranges\": [1.0, 1.001]}`. Non-finite floats are the strings `nan`, `inf`, `-inf`. Live `sample_messages` adds `_truncated_fields` (dotted paths of arrays, strings or bytes cut at `max_array_length`; a string of exactly that length plus `...` that was not cut is listed too) when something was cut, and `_raw_text` (the unparsed output) only when a message could not be parsed. With `arrays_summary_only` an array is a string like `<sequence type: float, length: 541>`. Large messages may be dropped to keep tool output bounded."
      • addedOutput schema / $defs / MessageSample / properties / received_ns
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "integer"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Wall-clock nanoseconds since epoch at which the `ros2` CLI printed this message, after it deserialized and formatted it: not the network receive time, and it lags more for large messages. Accurate to a fraction of a second. Live `sample_messages` only; `None` otherwise.",
        +  "title": "Received Ns"
        +}
      • addedOutput schema / $defs / MessageSample / properties / recorded_ns
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "integer"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Nanoseconds at which the recorder wrote this message into the bag (the bag's own record time, wall clock unless the recorder ran on simulated time). `peek_bag_samples` only; `None` for live samples, where `received_ns` plays that role.",
        +  "title": "Recorded Ns"
        +}
      • addedOutput schema / $defs / MessageSample / properties / stamp_source
        Added value: +{
        +  "anyOf": [
        +    {
        +      "enum": [
        +        "header",
        +        "none",
        +        "recorded"
        +      ],
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Where `timestamp_ns` comes from: `header` (the message's `header.stamp`), `none` (headerless live message, `timestamp_ns` is 0) or `recorded` (headerless message from a bag, `timestamp_ns` is the bag record time). `None` when the backend does not say.",
        +  "title": "Stamp Source"
        +}
      • changedOutput schema / $defs / MessageSample / properties / timestamp_ns / description
        Previous value: -"Timestamp in nanoseconds since epoch. In live mode this is the `header.stamp` of the sampled message when present: the live adapter invokes `ros2 topic echo --csv --once`, whose flattened CSV exposes `header.stamp.sec`/`nanosec` as the first two columns for any `Header`-stamped message. **Headerless message types** (e.g. `std_msgs/String`, `geometry_msgs/Twist`) carry no embedded timestamp, and `timestamp_ns` falls back to 0; an rclpy-backed adapter will eventually expose rmw receive times for those. Mock mode emits monotonically increasing values for deterministic ordering."New value: +"Nanoseconds from the message's own top-level `header.stamp` (`stamp_source` `header`), or 0 when the message has no header (`stamp_source` `none`, e.g. `std_msgs/String`, `geometry_msgs/Twist`). It is the publisher's clock, not the arrival time: on simulated time it is sim time since the simulation started (a few seconds, not a date) and 0 is a valid value. From `peek_bag_samples` a message without a header gets the bag record time instead (`stamp_source` `recorded`), also given in `recorded_ns`. Use `received_ns` for when the CLI printed the message. Mock samples with a header carry synthetic increasing values."
    • Changedpeek_dds_samples5 fields changed
      • changedOutput schema / $defs / MessageSample / properties / payload / description
        Previous value: -"Structured message payload. **In live mode the parser exposes the message fields as positional CSV columns** keyed as `col_0`, `col_1`, ... (`header.stamp.sec`/`nanosec` are stripped out into `timestamp_ns` when detected). The raw CSV row is preserved verbatim under the reserved `_raw_text` key so clients can re-parse against the message's IDL when needed. In mock mode the payload is fully structured and `_raw_text` is absent. Large messages (e.g. images) may be summarized to keep tool output bounded."New value: +"Message fields as nested named values, e.g. `{\"header\": {\"stamp\": {\"sec\": 12, \"nanosec\": 5}, \"frame_id\": \"base_laser\"}, \"ranges\": [1.0, 1.001]}`. Non-finite floats are the strings `nan`, `inf`, `-inf`. Live `sample_messages` adds `_truncated_fields` (dotted paths of arrays, strings or bytes cut at `max_array_length`; a string of exactly that length plus `...` that was not cut is listed too) when something was cut, and `_raw_text` (the unparsed output) only when a message could not be parsed. With `arrays_summary_only` an array is a string like `<sequence type: float, length: 541>`. Large messages may be dropped to keep tool output bounded."
      • addedOutput schema / $defs / MessageSample / properties / received_ns
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "integer"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Wall-clock nanoseconds since epoch at which the `ros2` CLI printed this message, after it deserialized and formatted it: not the network receive time, and it lags more for large messages. Accurate to a fraction of a second. Live `sample_messages` only; `None` otherwise.",
        +  "title": "Received Ns"
        +}
      • addedOutput schema / $defs / MessageSample / properties / recorded_ns
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "integer"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Nanoseconds at which the recorder wrote this message into the bag (the bag's own record time, wall clock unless the recorder ran on simulated time). `peek_bag_samples` only; `None` for live samples, where `received_ns` plays that role.",
        +  "title": "Recorded Ns"
        +}
      • addedOutput schema / $defs / MessageSample / properties / stamp_source
        Added value: +{
        +  "anyOf": [
        +    {
        +      "enum": [
        +        "header",
        +        "none",
        +        "recorded"
        +      ],
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Where `timestamp_ns` comes from: `header` (the message's `header.stamp`), `none` (headerless live message, `timestamp_ns` is 0) or `recorded` (headerless message from a bag, `timestamp_ns` is the bag record time). `None` when the backend does not say.",
        +  "title": "Stamp Source"
        +}
      • changedOutput schema / $defs / MessageSample / properties / timestamp_ns / description
        Previous value: -"Timestamp in nanoseconds since epoch. In live mode this is the `header.stamp` of the sampled message when present: the live adapter invokes `ros2 topic echo --csv --once`, whose flattened CSV exposes `header.stamp.sec`/`nanosec` as the first two columns for any `Header`-stamped message. **Headerless message types** (e.g. `std_msgs/String`, `geometry_msgs/Twist`) carry no embedded timestamp, and `timestamp_ns` falls back to 0; an rclpy-backed adapter will eventually expose rmw receive times for those. Mock mode emits monotonically increasing values for deterministic ordering."New value: +"Nanoseconds from the message's own top-level `header.stamp` (`stamp_source` `header`), or 0 when the message has no header (`stamp_source` `none`, e.g. `std_msgs/String`, `geometry_msgs/Twist`). It is the publisher's clock, not the arrival time: on simulated time it is sim time since the simulation started (a few seconds, not a date) and 0 is a valid value. From `peek_bag_samples` a message without a header gets the bag record time instead (`stamp_source` `recorded`), also given in `recorded_ns`. Use `received_ns` for when the CLI printed the message. Mock samples with a header carry synthetic increasing values."
    • Changedsample_messages9 fields changed
      • changedInput schema / properties / count / description
        Previous value: -"Maximum number of recent messages to return. Defaults to 5; silently clamped to 50 (the hard cap that keeps tool output bounded; read it from `health_check.max_sample_count`). Negative values raise an error. The returned `SampleResult.count` reflects the actual number of samples produced: it can be lower than the request (empty topic, timeout, mock fixture shorter than requested)."New value: +"Number of messages to wait for. Defaults to 5; at most 50 (a larger request is capped to 50 and `note` says so). 0 returns nothing. `SampleResult.count` is how many arrived: fewer than requested when the topic publishes slowly or the deadline (`timeout_s`) ran out."
      • addedInput schema / properties / count / maximum
        Added value: +50
      • changedInput schema / properties / max_array_length / description
        Previous value: -"Longest array, string or bytes value to return in full, 1..65536; longer ones are cut. A cut array is listed in the sample's `_truncated_after_columns` (index of the last kept column); a cut string or bytes value is kept as its first N characters plus `...` and listed in `_truncated_columns`. Defaults to 128, the `ros2 topic echo` default, which cuts a 541-beam `LaserScan` after 128 ranges. Pass null to return everything in full (large for images and point clouds; a message over the server's size cap, 1 MiB by default, is dropped with a note, and a very large message may not print within the echo timeout)."New value: +"Longest array, string or bytes value to return in full, 1..65536; longer ones are cut after their first N elements or characters and the field's dotted path is listed in the sample's `_truncated_fields`. Defaults to 128, the `ros2 topic echo` default, which cuts a 541-beam `LaserScan` after 128 ranges. Pass null to return everything in full (large for images and point clouds; a message over the server's size cap, 1 MiB by default, is dropped with a note, and a very large message may not print before the deadline)."
      • addedInput schema / properties / timeout_s
        Added value: +{
        +  "default": 10,
        +  "description": "Seconds to wait for the messages, 1..45, default 10. Bounds the whole call, topic lookup and the `ros2` CLI start-up (a few seconds on a slow machine) included: the call returns within about `timeout_s` + 2 s. Whatever arrived by then is returned with a `note`. Raise it for a topic that publishes slower than 1 Hz. The server handles one call at a time, so a long wait delays other tool calls.",
        +  "maximum": 45,
        +  "minimum": 1,
        +  "title": "Timeout S",
        +  "type": "number"
        +}
      • changedOutput schema / $defs / MessageSample / properties / payload / description
        Previous value: -"Structured message payload. **In live mode the parser exposes the message fields as positional CSV columns** keyed as `col_0`, `col_1`, ... (`header.stamp.sec`/`nanosec` are stripped out into `timestamp_ns` when detected). The raw CSV row is preserved verbatim under the reserved `_raw_text` key so clients can re-parse against the message's IDL when needed. In mock mode the payload is fully structured and `_raw_text` is absent. Large messages (e.g. images) may be summarized to keep tool output bounded."New value: +"Message fields as nested named values, e.g. `{\"header\": {\"stamp\": {\"sec\": 12, \"nanosec\": 5}, \"frame_id\": \"base_laser\"}, \"ranges\": [1.0, 1.001]}`. Non-finite floats are the strings `nan`, `inf`, `-inf`. Live `sample_messages` adds `_truncated_fields` (dotted paths of arrays, strings or bytes cut at `max_array_length`; a string of exactly that length plus `...` that was not cut is listed too) when something was cut, and `_raw_text` (the unparsed output) only when a message could not be parsed. With `arrays_summary_only` an array is a string like `<sequence type: float, length: 541>`. Large messages may be dropped to keep tool output bounded."
      • addedOutput schema / $defs / MessageSample / properties / received_ns
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "integer"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Wall-clock nanoseconds since epoch at which the `ros2` CLI printed this message, after it deserialized and formatted it: not the network receive time, and it lags more for large messages. Accurate to a fraction of a second. Live `sample_messages` only; `None` otherwise.",
        +  "title": "Received Ns"
        +}
      • addedOutput schema / $defs / MessageSample / properties / recorded_ns
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "integer"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Nanoseconds at which the recorder wrote this message into the bag (the bag's own record time, wall clock unless the recorder ran on simulated time). `peek_bag_samples` only; `None` for live samples, where `received_ns` plays that role.",
        +  "title": "Recorded Ns"
        +}
      • addedOutput schema / $defs / MessageSample / properties / stamp_source
        Added value: +{
        +  "anyOf": [
        +    {
        +      "enum": [
        +        "header",
        +        "none",
        +        "recorded"
        +      ],
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "description": "Where `timestamp_ns` comes from: `header` (the message's `header.stamp`), `none` (headerless live message, `timestamp_ns` is 0) or `recorded` (headerless message from a bag, `timestamp_ns` is the bag record time). `None` when the backend does not say.",
        +  "title": "Stamp Source"
        +}
      • changedOutput schema / $defs / MessageSample / properties / timestamp_ns / description
        Previous value: -"Timestamp in nanoseconds since epoch. In live mode this is the `header.stamp` of the sampled message when present: the live adapter invokes `ros2 topic echo --csv --once`, whose flattened CSV exposes `header.stamp.sec`/`nanosec` as the first two columns for any `Header`-stamped message. **Headerless message types** (e.g. `std_msgs/String`, `geometry_msgs/Twist`) carry no embedded timestamp, and `timestamp_ns` falls back to 0; an rclpy-backed adapter will eventually expose rmw receive times for those. Mock mode emits monotonically increasing values for deterministic ordering."New value: +"Nanoseconds from the message's own top-level `header.stamp` (`stamp_source` `header`), or 0 when the message has no header (`stamp_source` `none`, e.g. `std_msgs/String`, `geometry_msgs/Twist`). It is the publisher's clock, not the arrival time: on simulated time it is sim time since the simulation started (a few seconds, not a date) and 0 is a valid value. From `peek_bag_samples` a message without a header gets the bag record time instead (`stamp_source` `recorded`), also given in `recorded_ns`. Use `received_ns` for when the CLI printed the message. Mock samples with a header carry synthetic increasing values."
  3. 12 tool updatesv0.5.6
    • First observedanalyze_bag
    • First observeddetect_qos_mismatches
    • First observedget_topic_info
    • First observedhealth_check
    • First observedlist_endpoints
    • First observedlist_participants
    • First observedlist_topics
    • First observedparticipant_events
    • First observedpeek_bag_samples
    • First observedpeek_dds_samples
    • First observedsample_messages
    • First observedtopic_metrics

TDQS

A4.4/5.0

Scored across 14 tools

Disambiguation4/5

Most tools have clearly distinct layer-and-action purposes: ROS 2 graph vs DDS bus, current state vs lifecycle events, summary vs samples, live vs bag. However, the sample/peek trio (sample_messages, peek_dds_samples, peek_bag_samples) and list-vs-get pairs share enough surface concepts that an agent may briefly hesitate, even though the descriptions cross-reference the correct choice.

Naming Consistency4/5

All names use snake_case and most follow a verb_noun pattern (list_*, get_*, peek_*, sample_messages, analyze_bag, detect_*). A few established noun forms (health_check, participant_events, topic_metrics) deviate from the verb_noun convention but remain readable and predictable.

Tool Count5/5

14 tools is well within the expected range for a rich ROS 2/DDS diagnostic server. Each tool covers a distinct layer, lifecycle stage, or data source, so the set does not feel padded or thin.

Completeness4/5

Coverage is broad: environment health, ROS nodes/topics/info/samples, DDS participants/endpoints/events/samples/metrics/QoS mismatch detection, and bag analysis/peek. Minor gaps remain, such as no direct global list_services or list_actions, and DDS user-topic payloads are undecodable by design, which limits some diagnostic depth.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Read-only Modbus TCP monitoring server that exposes safe MCP tools for AI agents to read holding/input registers, coils, and device identity from industrial devices without write access.
    4
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    Read-only MCP server that exposes Autopsy digital forensics case data as tools, enabling LLM clients like Cline to browse filesystems, query artifacts, and search keywords without modifying the case.
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A read-only MCP server for bounded semantic inspection of ROS 2 perception systems, enabling discovery and metadata extraction from sensors such as cameras, depth sensors, and LiDARs without device configuration or actuation.
    Apache 2.0