splashdown_query reads one topic over an exact MCAP log-time window. It works without ROS installed and supports indexed recordings larger than the existing 64 MiB whole-recording MCP limit. The result is an observation, not a robot-health, simulation, or acceptance verdict.
Use splashdown query:
splashdown query incident.mcap --root /path/to/evidence \
--topic /odom --start-ns 1700000000000000000 --end-ns 1700000002000000000 \
--field twist.twist.linear.x --reduction mean --unit m/s --max-samples 20It prints one JSON document and exits 0 for a complete observation or 3 for partial/unavailable evidence. Both window endpoints are integer nanoseconds on the recording's log_time clock. The start is inclusive and the end is exclusive. These are not relative seconds, ROS header timestamps, or an inferred run clock.
| CLI argument / Python or MCP key | Default and constraint |
|---|---|
recording | Required path. Relative paths resolve under the configured root. |
--root / root | CLI defaults to the current directory. Required Python keyword. MCP uses the server's --root, not a per-call argument. |
--topic / topic | Required absolute topic name, up to 512 characters. |
--start-ns / start_ns | Required integer from 0 through 2**64 - 1, inclusive. |
--end-ns / end_ns | Required integer from 1 through 2**64, inclusive; must exceed the start by at most 300 seconds. |
--field / field | Unset. Dotted public field names, at most 16 components and 256 characters. |
--reduction / reduction | count without a field, mean with a field. count, min, max, mean, last are supported. |
--unit / unit | Unset. Up to 64 printable characters; requires a field. |
--max-messages / max_messages | 10,000. Integer from 1 through 10,000. |
--max-samples / max_samples | 0. Integer from 0 through 200. |
--json | Accepted for consistency with other splashdown commands; output is always JSON. |
Argparse errors, such as missing required arguments or a misspelled reduction, exit 2 and print usage. Requests parsed successfully but refused by the query, such as an oversized interval, normally emit an incomplete JSON record and exit 3. No output file is created unless the caller redirects stdout or saves the result. Use a new filename for that redirection; normal shell > can replace a file.
splashdown query supervises a disposable worker process with a 25-second outer timeout. Normal output is the direct evidence-window record. If that process fails before producing a usable record, splashdown query emits an incomplete flightops.tool-result/0 query error envelope and exits 3. Both forms can be saved for the shared results reader.
The equivalent MCP call uses the existing Flightops stdio server:
{
"name": "splashdown_query",
"arguments": {
"recording": "incident.mcap",
"topic": "/odom",
"start_ns": 1700000000000000000,
"end_ns": 1700000002000000000,
"field": "twist.twist.linear.x",
"reduction": "mean",
"unit": "m/s",
"max_samples": 20
}
}recording, topic, start_ns, and end_ns are required. With no field, the query returns message count, first/last timestamp, rate over observed samples, and the largest gap between consecutive samples. It does not include silence between a window boundary and the first/last sample in that gap measurement.
The example is a tools/call params object; an MCP client supplies the JSON-RPC wrapper. The server returns the envelope in both structuredContent and a JSON text content block. Save the structuredContent object itself for flightops-results or flightops-tui, not the entire transport-level response. MCP marks blocked and unavailable results with isError: true even when partial observations are useful.
An optional dotted field path selects a finite number or boolean from embedded ROS 2 CDR/ros2msg messages. Supported reductions are count, min, max, mean, and last; the default for a field is mean. Booleans become 0/1. Expressions, array access, private attributes, cross-topic calculations, and nonnumeric fields are refused. last follows log time, with file position breaking timestamp ties.
Units are not guessed from a field name. unit is a caller declaration and is labeled caller-declared. Without it, the field/reduction unit stays null. Timing and count units are always explicit. The result also names recorded message types and encodings. publish_time_ns is preserved in optional samples, with no claim that its clock matches the recorder or a message header.
Reading the result
MCP preserves the existing flightops.tool-result/0 envelope. Its data is a flightops.evidence-window/0 record, the same document printed by splashdown query. Numerical reductions and statistics are computed by Python, not an agent.
| Outcome | Meaning |
|---|---|
observed, complete: true | All matching messages in the supported window were examined. |
empty_window, complete: true | The channel exists, with zero matching messages. Count is zero; undefined rates and numeric reductions are null. |
missing_topic, complete: false | No recorded channel exists. Measurements stay null rather than becoming zero. |
message_limit, truncated: true | The message cap stopped the query. Statistics cover an observed prefix; the final reduction is null. |
message_size_limit, complete: false | One selected payload exceeds 1 MiB. Status is unavailable, measurements are null and truncated is false. This is not a measured count-prefix result. |
| Other incomplete outcomes | An index, encoding, integrity, resource, path, or input error prevented the requested observation. The reason is explicit. |
Complete outcomes have status: pass. missing_topic and a measured message_limit prefix have status: blocked; other refusals have status: unavailable. truncated is true for a count-prefix limit or an output limit, and false for ordinary refusals. Always inspect complete and outcome together rather than assuming any returned count is final.
| Top-level field | Meaning |
|---|---|
schema | flightops.evidence-window/0. |
status, complete, truncated, outcome | Observation status, coverage and the machine-readable reason. |
reason | Explanation on incomplete results; absent on complete observations. |
request | Validated topic, exact bounds, interval, field, reduction and message/sample caps; null if the request was refused before validation. |
source | File identity, recorded channel/schema metadata and inspected-byte accounting; null if unavailable. |
measurements | Count, observed scope, first/last log times, rate, largest gap and reduction; null when no measurement can be established. |
samples | Bounded list of log_time_ns, publish_time_ns and value. Value is null for a count-only query. |
samples_elided | Number of measured messages omitted from samples, when measurements were produced. |
clock, units | Clock conventions and explicit metric/field units. |
limits | Effective fixed file/read/summary/chunk/expanded/message/time/output limits. |
For n messages with distinct first and last timestamps, rate is (n - 1) * 1_000_000_000 / (last_log_time_ns - first_log_time_ns). One message has no rate or gap. Coincident timestamps have a null rate and zero gap when there are at least two messages. Empty count reduction is exactly zero; empty min, max, mean and last reductions are null. Mean uses Python's compensated math.fsum over scaled values. Numeric overflow is refused instead of returning NaN or infinity.
max_messages defaults to 10,000 and may only lower that cap. max_samples defaults to zero and accepts at most 200. Sample elision alone does not make a fully computed reduction incomplete. samples_elided reports omitted samples among observed messages; it does not estimate unseen messages after a limit. Partial samples come from a file-order prefix, sorted by timestamp for display.
source includes the resolved file path, size, modification time, summary SHA-256, and a SHA-256 over inspected chunk bytes. hashed_chunks names their offsets and lengths; the digest incorporates each offset as an unsigned little-endian 64-bit integer followed by that chunk's raw bytes, in file order. The summary hash covers the bytes from summary_start through the end of summary offsets, excluding the footer. File metadata is checked again after reading.
This identity covers a local file version and the inspected evidence. It does not authenticate its producer or hash/check unread data; whole_file_sha256 is explicitly null. A finalized MCAP footer and valid summary are required. Available summary and selected-chunk CRCs are verified. A larger proof or incident manifest can provide a separately established whole-file identity.
Resource bounds
| Resource | Hard query limit |
|---|---|
| Recording size | 32 GiB |
| Window duration | 300 seconds |
| Cumulative file reads | 64 MiB |
| Summary bytes / records | 4 MiB / 4,096 |
| Channels / schemas | 1,024 each |
| Embedded schema | 256 KiB |
| Selected chunks | 64 |
| Compressed or expanded chunk | 8 MiB, plus a bounded chunk header |
| Total expanded selected chunks | 32 MiB |
| Inspected chunk records | 100,000 |
| Selected message payload | 1 MiB |
| Matching messages / returned samples | 10,000 / 200 |
| Query JSON | 192 KiB |
| POSIX process address space | 512 MiB |
| Query time | 20 seconds; MCP also keeps its outer configured deadline |
Limits are deliberate refusals, not invitations to raise every cap. Narrow the window or create a smaller recording when a query cannot fit. The 64 MiB input cap on splashdown_stats and splashdown_assert remains unchanged, as does the 256 KiB MCP envelope output cap.
The query reads the bounded summary through MCAP's indexed reader and seeks directly to matching chunks. It does not construct splashdown's whole-recording timestamp index. Chunk headers are checked against indexes before bounded decompression. Supported compression is uncompressed, Zstandard, and LZ4. Missing indexes and unfinished/truncated files are refused; there is no automatic full-file scan or recovery. Numeric decoding currently supports only embedded ros2msg schemas and CDR messages.
splashdown query and the MCP worker apply process limits on POSIX. MCP also runs each request in a disposable process and kills timed-out workers. The in-process query_window function has read/compute checks, but callers embedding it must supply the same disposable process boundary for untrusted decoders. Paths must resolve inside --root, including symlinks. Only regular files are accepted; FIFOs/devices and escapes outside the root are refused. The query never changes the recording.
Python API
from pathlib import Path
from splashdown.query import query_window
observation = query_window(
"incident.mcap",
root=Path("/path/to/evidence"),
topic="/odom",
start_ns=1700000000000000000,
end_ns=1700000002000000000,
field="twist.twist.linear.x",
reduction="mean",
unit="m/s",
max_messages=10000,
max_samples=20,
)
if observation["complete"]:
print(observation["measurements"]["reduction"])
else:
print(observation["outcome"], observation["reason"])query_window returns a JSON-compatible dictionary for normal query refusals; it does not turn missing evidence into an exception-only interface. In a long-lived Python service, call it in a supervised child process. apply_process_limits() sets process-wide address-space and signal limits and is intended for that child, the standalone CLI or the MCP worker, not an already-running application process.
Saved-result integration
splashdown.query_result.validate_window(data) checks saved records before they are presented. It refuses contradictory status/completeness/truncation flags, invented clock mappings, nonfinite measurements, impossible count/timing values, missing provenance and inconsistent sample elision. When every sample is present, it also checks the reported reduction against those samples.
splashdown.query_result.presentation(data, source) returns the shared results model for HTML, TUI and plain output. It preserves zero measurements and null values, labels the result as an observation, and includes the original JSON. Neither function opens paths named by the evidence record. Validation checks the record's internal consistency; it does not authenticate its producer or reread the recording to recompute omitted measurements.
Both direct query JSON and a saved splashdown_query MCP envelope are supported:
splashdown query incident.mcap --root /path/to/evidence \
--topic /odom --start-ns 1700000000000000000 --end-ns 1700000002000000000 \
--max-samples 20 > /tmp/odom-window.json
flightops-results /tmp/odom-window.json -o /tmp/odom-window.html
flightops-tui --plain /tmp/odom-window.jsonFor saved MCP queries, the shared loader validates the inner evidence-window record and requires outer status and complete to agree with it. A wrapper cannot turn a partial observation into a complete one. A timeout or crashed worker may instead return only an error envelope; that remains readable only as unavailable, complete: false, assurance: none, with a nonempty error and no unsupported inner schema. The UI labels both forms as observations, never as executed simulation results.
Python models and TUI JSON preserve integer values. HTML uses decimal strings for presentation integers outside JavaScript's safe range, so timestamps such as 1700000000000000000 and 1700000000000000010 remain distinct. The raw panel and download use the original UTF-8 JSON text, including exact numeric tokens, whitespace and line endings. Browser presentation strings are not a new wire schema. A JavaScript MCP client must also preserve large JSON integers before saving them; the viewer cannot recover precision lost earlier in the client.
Troubleshooting and verification
| Outcome or symptom | Next step |
|---|---|
missing_topic | Check the exact recorded topic name. This is distinct from an existing topic with a complete zero-count interval. |
empty_window | Check clock basis and exact nanosecond bounds. The query does not translate relative run seconds. |
message_limit | Split the time interval into smaller windows; prefix reductions are deliberately unavailable. |
message_size_limit | The selected payload is too large for this query path. Use a supported offline workflow that can handle the payload; reducing sample output does not reduce payload size. |
unindexed_recording, invalid_recording, unsupported_summary | Use a finalized supported MCAP. Recovery or reindexing is a separate operation and changes evidence that must receive its own identity. |
chunk_limit, window_limit, read_limit, record_limit | Narrow the interval. A narrower interval may still touch the same oversized chunk. |
unsupported_encoding, missing_field, non_numeric_field | Try a count-only query, or choose a supported numeric field from the embedded schema. Expressions and array indexing are unsupported. |
invalid_path | Put the recording under the configured root and pass its correct relative path. Symlink escapes are refused. |
source_changed | Finish recording and query an unchanged file. |
time_limit, memory_limit, worker timeout | Reduce the query or use a reviewed smaller recording. An outer MCP timeout does not increase internal limits. |
| Saved record refused by the viewer | Keep the original tool result intact. Inspect status/completeness, schema and finite-number errors; do not edit flags to force a green display. |
Focused regression tests need a source checkout: they are in splashdown/tests/ and integrations/tests/, which are not in the install. From the repository root:
.venv/bin/python -m pytest splashdown/tests/test_query.py -q
.venv/bin/python -m pytest \
integrations/tests/test_queries.py integrations/tests/test_results_query.py -qThe tests generate real indexed MCAP, including a recording larger than 64 MiB, and exercise bounded reads, zero/absent/partial data, numeric/clock/unit semantics, compressed and malformed inputs, path refusal, MCP envelopes and exact HTML JSON. These are query and rendering checks, not closed-loop simulation acceptance.