fix(perception): bound LAB replay to AI window
This commit is contained in:
parent
10019a454a
commit
082b7057da
|
|
@ -113,12 +113,17 @@ changing the AI result selected for its source recording. A LAB instance binds:
|
||||||
- run and publication timestamps plus JSON provenance;
|
- run and publication timestamps plus JSON provenance;
|
||||||
- a content-addressed compute job, LiDAR pack and integrated visual result.
|
- a content-addressed compute job, LiDAR pack and integrated visual result.
|
||||||
|
|
||||||
The source evidence remains the only raw sensor master. A LAB replay cache and
|
The source evidence remains the only raw sensor master. Full-session LAB replay
|
||||||
unchanged camera inputs use hard links on the same filesystem; no second copy of
|
caches and unchanged camera inputs use hard links on the same filesystem; no
|
||||||
the source RRD, video or native K1 capture is created. Derived arrays are stored
|
second copy of the source RRD, video or native K1 capture is created. A bounded
|
||||||
only when the run did not already persist a viewer-ready form. Deleting a LAB
|
experiment instead stores a small derived RRD slice with exact start/end markers
|
||||||
catalog entry cannot delete the referenced source session, and a source session
|
and omits the source's unbounded recorded-video descriptor. Its integrated
|
||||||
with published LAB instances fails closed on deletion.
|
perception overlay owns the matching bounded camera. This prevents Rerun from
|
||||||
|
holding the final AI frame while a longer source cloud continues to play.
|
||||||
|
Derived arrays are stored only when the run did not already persist a
|
||||||
|
viewer-ready form. Deleting a LAB catalog entry cannot delete the referenced
|
||||||
|
source session, and a source session with published LAB instances fails closed
|
||||||
|
on deletion.
|
||||||
|
|
||||||
The visual result is resolved by the LAB session id, so `LAB E19` and `LAB E21`
|
The visual result is resolved by the LAB session id, so `LAB E19` and `LAB E21`
|
||||||
can coexist over `RAVNOVES00` without “latest accepted result” replacing an
|
can coexist over `RAVNOVES00` without “latest accepted result” replacing an
|
||||||
|
|
@ -132,7 +137,11 @@ not duplicate mask pixels. Its visual publication materializes a mask only
|
||||||
after an exact SHA-256 match against the immutable accepted E19 semantic
|
after an exact SHA-256 match against the immutable accepted E19 semantic
|
||||||
reference. The seven detector frames replaced by the bounded latest-wins queue
|
reference. The seven detector frames replaced by the bounded latest-wins queue
|
||||||
remain explicit empty frames. This preserves the measured near-real-time
|
remain explicit empty frames. This preserves the measured near-real-time
|
||||||
behavior rather than presenting a fabricated gap-free run.
|
behavior rather than presenting a fabricated gap-free run. The saved-session
|
||||||
|
duration and base RRD range must equal the E21 visual result range; attaching
|
||||||
|
the 60-second result to the complete 8:55 source replay is an invalid
|
||||||
|
presentation because latest-at image data would appear static after the E21
|
||||||
|
window ends.
|
||||||
|
|
||||||
The first preparation of a long capture can take time because every decodable
|
The first preparation of a long capture can take time because every decodable
|
||||||
point/pose message and every valid K1 `ModelingReport` is projected into RRD.
|
point/pose message and every valid K1 `ModelingReport` is projected into RRD.
|
||||||
|
|
|
||||||
|
|
@ -187,32 +187,43 @@ so that visual improvements cannot hide a runtime regression.
|
||||||
|
|
||||||
## Immutable Mission Core publication
|
## Immutable Mission Core publication
|
||||||
|
|
||||||
E21 is published as the separate saved-session instance
|
The original presentation `lab-e21-d0201712` is retained as historical
|
||||||
`lab-e21-d0201712`, displayed as
|
evidence: it attached the bounded E21 overlay to the complete 8:55 source
|
||||||
`LAB E21 · Real-time 1× · RTX 4090 · d0201712`. It does not replace the
|
replay. Its 601 camera/AI frames are dynamic between 35.421857 and 95.383857
|
||||||
result bound to `RAVNOVES00`, and it coexists with the comparison baseline
|
seconds, but Rerun then holds the last image while the source cloud continues.
|
||||||
|
It is not the correct operator comparison view.
|
||||||
|
|
||||||
|
The synchronized publication is the separate saved-session instance
|
||||||
|
`lab-e21-window2-d0201712`, displayed as
|
||||||
|
`LAB E21.3 · Real-time 1× · synchronized 60s · d0201712`. It keeps the
|
||||||
|
source result immutable, materializes a bounded 4.43 MB spatial RRD and ends
|
||||||
|
the base scene on the exact final AI timestamp. It does not replace
|
||||||
|
`RAVNOVES00`, the historical E21 presentation or the comparison baseline
|
||||||
`lab-e19-36964643`.
|
`lab-e19-36964643`.
|
||||||
|
|
||||||
| Publication item | Value |
|
| Publication item | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| LAB source session | `20260720T065719Z_viewer_live` |
|
| LAB source session | `20260720T065719Z_viewer_live` |
|
||||||
| Visual result | `e10-integrated-perception-d0ecafd5e56f8ef132b8cb88f45ec275aa17470c682c6cce7c08f5597bfd7bd6` |
|
| Synchronized LAB session | `lab-e21-window2-d0201712` |
|
||||||
|
| Visual result | `e10-integrated-perception-5cbba0b9839ae843c05607cec8448a2b43464eb81edeb11cf7c01cfec8464c75` |
|
||||||
| Visual frames | 601 |
|
| Visual frames | 601 |
|
||||||
| Worker result frames | 594 |
|
| Worker result frames | 594 |
|
||||||
| Explicit latest-wins replacement frames | 12, 239–242, 433, 498 |
|
| Explicit latest-wins replacement frames | 12, 239–242, 433, 498 |
|
||||||
| SHA-matched semantic masks | 121 / 121 |
|
| SHA-matched semantic masks | 121 / 121 |
|
||||||
| Accepted cuboids materialized | 881 |
|
| Accepted cuboids materialized | 881 |
|
||||||
| Raw source payloads copied | no |
|
| Raw source payloads copied | no |
|
||||||
| Source RRD storage | same inode through hard link |
|
| Replay range | 35.421857292–95.383857292 s |
|
||||||
| Base replay readiness | 0.03 s observed |
|
| Saved-session duration | 59.962 s |
|
||||||
| Initial visual overlay publication | 111.47 s observed |
|
| Bounded spatial RRD | 4,429,382 bytes |
|
||||||
| Warm visual overlay retrieval | 0.04 s observed |
|
| Initial visual overlay publication | 144.54 s observed |
|
||||||
|
| Warm visual overlay retrieval | 0.046 s observed |
|
||||||
|
|
||||||
The 111.47-second step is viewer-container publication from already computed
|
The 144.54-second step is viewer-container publication from already computed
|
||||||
results, not detector, semantic or fusion inference. It is completed before
|
results, not detector, semantic or fusion inference. It is completed before
|
||||||
operator handoff. Warm UI openings reuse the resulting 7.88 MB Rerun overlay.
|
operator handoff. Warm UI openings reuse the resulting 7,882,010-byte Rerun
|
||||||
The base source remains the full 8:55.718 recording; E21 AI output covers the
|
overlay. The synchronized base recording contains exact range markers, so the
|
||||||
accepted 59.994-second envelope beginning at source camera time 35.421857 s.
|
host timeline is a 59.962-second comparison window and cannot advance into a
|
||||||
|
static post-E21 tail.
|
||||||
|
|
||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,8 @@ def publish_e21_lab_instance(
|
||||||
store.data_dir,
|
store.data_dir,
|
||||||
source_session_id=reference.job.session_id,
|
source_session_id=reference.job.session_id,
|
||||||
lab_session_id=lab_session_id,
|
lab_session_id=lab_session_id,
|
||||||
|
timeline_start_ns=round(validated.timeline_start_seconds * 1_000_000_000),
|
||||||
|
timeline_end_ns=round(validated.timeline_end_seconds * 1_000_000_000),
|
||||||
)
|
)
|
||||||
binding = store.publish_lab_instance(
|
binding = store.publish_lab_instance(
|
||||||
session_id=lab_session_id,
|
session_id=lab_session_id,
|
||||||
|
|
@ -210,9 +212,13 @@ def publish_e21_lab_instance(
|
||||||
source_result_id=str(e21_document["result_id"]),
|
source_result_id=str(e21_document["result_id"]),
|
||||||
config_sha256=str(e21_report["identity"]["profile_sha256"]),
|
config_sha256=str(e21_report["identity"]["profile_sha256"]),
|
||||||
run_created_at_utc=str(e21_report["created_at_utc"]),
|
run_created_at_utc=str(e21_report["created_at_utc"]),
|
||||||
|
duration_seconds=(
|
||||||
|
validated.timeline_end_seconds - validated.timeline_start_seconds
|
||||||
|
),
|
||||||
|
include_recorded_media=False,
|
||||||
provenance={
|
provenance={
|
||||||
"schema_version": "missioncore.e21-lab-publication/v1",
|
"schema_version": "missioncore.e21-lab-publication/v1",
|
||||||
"storage_mode": "hard-linked-source-and-bounded-derived-projection",
|
"storage_mode": "bounded-derived-replay-and-projection",
|
||||||
"e21_result_id": e21_document["result_id"],
|
"e21_result_id": e21_document["result_id"],
|
||||||
"worker_result_id": worker_document["result_id"],
|
"worker_result_id": worker_document["result_id"],
|
||||||
"semantic_reference_result_id": reference.result_id,
|
"semantic_reference_result_id": reference.result_id,
|
||||||
|
|
@ -227,6 +233,8 @@ def publish_e21_lab_instance(
|
||||||
498,
|
498,
|
||||||
],
|
],
|
||||||
"visual_frame_count": frame_count,
|
"visual_frame_count": frame_count,
|
||||||
|
"timeline_start_seconds": validated.timeline_start_seconds,
|
||||||
|
"timeline_end_seconds": validated.timeline_end_seconds,
|
||||||
"source_payloads_mutated": False,
|
"source_payloads_mutated": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ from __future__ import annotations
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -24,14 +28,35 @@ def publish_lab_replay_cache(
|
||||||
*,
|
*,
|
||||||
source_session_id: str,
|
source_session_id: str,
|
||||||
lab_session_id: str,
|
lab_session_id: str,
|
||||||
|
timeline_start_ns: int | None = None,
|
||||||
|
timeline_end_ns: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Hard-link a validated source RRD and clone its path-free sidecars.
|
"""Publish a validated source RRD and clone its path-free sidecars.
|
||||||
|
|
||||||
The RRD bytes are immutable and independent of the catalog id, so a hard
|
Full-session LAB instances hard-link the immutable source bytes. Bounded
|
||||||
link gives every LAB instance a normal cache endpoint without consuming a
|
experiments materialize only their declared timeline window, so a short AI
|
||||||
second copy of the multi-gigabyte recording.
|
run cannot silently hold its final frame over the rest of a long source
|
||||||
|
recording.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
bounded = timeline_start_ns is not None or timeline_end_ns is not None
|
||||||
|
if bounded and (
|
||||||
|
not isinstance(timeline_start_ns, int)
|
||||||
|
or isinstance(timeline_start_ns, bool)
|
||||||
|
or not isinstance(timeline_end_ns, int)
|
||||||
|
or isinstance(timeline_end_ns, bool)
|
||||||
|
or timeline_start_ns < 0
|
||||||
|
or timeline_end_ns <= timeline_start_ns
|
||||||
|
):
|
||||||
|
raise ValueError("LAB replay window is invalid")
|
||||||
|
window_start_ns = -1
|
||||||
|
window_end_ns = -1
|
||||||
|
if bounded:
|
||||||
|
assert isinstance(timeline_start_ns, int)
|
||||||
|
assert isinstance(timeline_end_ns, int)
|
||||||
|
window_start_ns = timeline_start_ns
|
||||||
|
window_end_ns = timeline_end_ns
|
||||||
|
|
||||||
root = data_dir.expanduser().resolve(strict=True)
|
root = data_dir.expanduser().resolve(strict=True)
|
||||||
recordings = (root / "recordings").resolve(strict=True)
|
recordings = (root / "recordings").resolve(strict=True)
|
||||||
source_root = recordings / source_session_id
|
source_root = recordings / source_session_id
|
||||||
|
|
@ -52,10 +77,32 @@ def publish_lab_replay_cache(
|
||||||
raise SessionIntegrityError("LAB recording cache root is unsafe")
|
raise SessionIntegrityError("LAB recording cache root is unsafe")
|
||||||
target_rrd = target_root / RECORDING_CACHE_FILENAME
|
target_rrd = target_root / RECORDING_CACHE_FILENAME
|
||||||
if target_rrd.exists():
|
if target_rrd.exists():
|
||||||
|
if bounded:
|
||||||
|
_require_matching_bounded_cache(
|
||||||
|
target_rrd,
|
||||||
|
target_root / RECORDING_CACHE_SIDECAR_FILENAME,
|
||||||
|
lab_session_id=lab_session_id,
|
||||||
|
timeline_start_ns=window_start_ns,
|
||||||
|
timeline_end_ns=window_end_ns,
|
||||||
|
)
|
||||||
|
else:
|
||||||
source_stat = _require_regular(source_rrd, source_root)
|
source_stat = _require_regular(source_rrd, source_root)
|
||||||
target_stat = _require_regular(target_rrd, target_root)
|
target_stat = _require_regular(target_rrd, target_root)
|
||||||
if (source_stat.st_dev, source_stat.st_ino) != (target_stat.st_dev, target_stat.st_ino):
|
if (source_stat.st_dev, source_stat.st_ino) != (
|
||||||
raise SessionIntegrityError("LAB recording cache already contains different bytes")
|
target_stat.st_dev,
|
||||||
|
target_stat.st_ino,
|
||||||
|
):
|
||||||
|
raise SessionIntegrityError(
|
||||||
|
"LAB recording cache already contains different bytes"
|
||||||
|
)
|
||||||
|
elif bounded:
|
||||||
|
_publish_bounded_rrd(
|
||||||
|
source_rrd,
|
||||||
|
target_rrd,
|
||||||
|
recording_id=lab_session_id,
|
||||||
|
timeline_start_ns=window_start_ns,
|
||||||
|
timeline_end_ns=window_end_ns,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
temporary = target_root / f".{RECORDING_CACHE_FILENAME}.{os.getpid()}.tmp"
|
temporary = target_root / f".{RECORDING_CACHE_FILENAME}.{os.getpid()}.tmp"
|
||||||
try:
|
try:
|
||||||
|
|
@ -67,10 +114,17 @@ def publish_lab_replay_cache(
|
||||||
cloned = {
|
cloned = {
|
||||||
**document,
|
**document,
|
||||||
"session_id": lab_session_id,
|
"session_id": lab_session_id,
|
||||||
|
"recording_byte_length": target_stat.st_size,
|
||||||
"recording_mtime_ns": target_stat.st_mtime_ns,
|
"recording_mtime_ns": target_stat.st_mtime_ns,
|
||||||
|
"recording_sha256": _sha256(target_rrd),
|
||||||
|
"timeline_start_ns": (
|
||||||
|
window_start_ns if bounded else document["timeline_start_ns"]
|
||||||
|
),
|
||||||
|
"timeline_end_ns": window_end_ns if bounded else document["timeline_end_ns"],
|
||||||
}
|
}
|
||||||
write_json_atomic(target_root / RECORDING_CACHE_SIDECAR_FILENAME, cloned)
|
write_json_atomic(target_root / RECORDING_CACHE_SIDECAR_FILENAME, cloned)
|
||||||
os.chmod(target_root / RECORDING_CACHE_SIDECAR_FILENAME, 0o600)
|
os.chmod(target_root / RECORDING_CACHE_SIDECAR_FILENAME, 0o600)
|
||||||
|
if not bounded:
|
||||||
_clone_recorded_media_sidecars(
|
_clone_recorded_media_sidecars(
|
||||||
root / "recorded-media-preparations",
|
root / "recorded-media-preparations",
|
||||||
source_session_id=source_session_id,
|
source_session_id=source_session_id,
|
||||||
|
|
@ -78,6 +132,180 @@ def publish_lab_replay_cache(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _publish_bounded_rrd(
|
||||||
|
source_rrd: Path,
|
||||||
|
target_rrd: Path,
|
||||||
|
*,
|
||||||
|
recording_id: str,
|
||||||
|
timeline_start_ns: int,
|
||||||
|
timeline_end_ns: int,
|
||||||
|
) -> None:
|
||||||
|
rerun = _rerun_cli()
|
||||||
|
end_exclusive_ns = timeline_end_ns + 1
|
||||||
|
split_recording_prefix = f"{recording_id}-window-"
|
||||||
|
split_recording_id = f"{split_recording_prefix}1"
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix=".lab-window-",
|
||||||
|
dir=target_rrd.parent,
|
||||||
|
) as temporary_name:
|
||||||
|
temporary_root = Path(temporary_name)
|
||||||
|
split_root = temporary_root / "split"
|
||||||
|
split_root.mkdir()
|
||||||
|
_run_rerun(
|
||||||
|
rerun,
|
||||||
|
"rrd",
|
||||||
|
"split",
|
||||||
|
"--output-dir",
|
||||||
|
str(split_root),
|
||||||
|
"--timeline",
|
||||||
|
"session_time",
|
||||||
|
"--time",
|
||||||
|
str(timeline_start_ns),
|
||||||
|
"--time",
|
||||||
|
str(end_exclusive_ns),
|
||||||
|
"--recording-id",
|
||||||
|
split_recording_prefix,
|
||||||
|
str(source_rrd),
|
||||||
|
)
|
||||||
|
split_name = (
|
||||||
|
f"{source_rrd.stem}_{_rrd_duration(timeline_start_ns)}"
|
||||||
|
f"__{_rrd_duration(end_exclusive_ns)}.rrd"
|
||||||
|
)
|
||||||
|
split_path = split_root / split_name
|
||||||
|
_require_regular(split_path, split_root)
|
||||||
|
marker = temporary_root / "window.rrd"
|
||||||
|
_write_window_markers(
|
||||||
|
marker,
|
||||||
|
recording_id=split_recording_id,
|
||||||
|
timeline_start_ns=timeline_start_ns,
|
||||||
|
timeline_end_ns=timeline_end_ns,
|
||||||
|
)
|
||||||
|
merged = temporary_root / "bounded.rrd"
|
||||||
|
_run_rerun(
|
||||||
|
rerun,
|
||||||
|
"rrd",
|
||||||
|
"merge",
|
||||||
|
"--output",
|
||||||
|
str(merged),
|
||||||
|
str(split_path),
|
||||||
|
str(marker),
|
||||||
|
)
|
||||||
|
_run_rerun(rerun, "rrd", "verify", str(merged))
|
||||||
|
_require_regular(merged, temporary_root)
|
||||||
|
temporary_target = target_rrd.parent / f".{target_rrd.name}.{os.getpid()}.tmp"
|
||||||
|
try:
|
||||||
|
os.replace(merged, temporary_target)
|
||||||
|
os.chmod(temporary_target, 0o600)
|
||||||
|
os.replace(temporary_target, target_rrd)
|
||||||
|
finally:
|
||||||
|
temporary_target.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_window_markers(
|
||||||
|
path: Path,
|
||||||
|
*,
|
||||||
|
recording_id: str,
|
||||||
|
timeline_start_ns: int,
|
||||||
|
timeline_end_ns: int,
|
||||||
|
) -> None:
|
||||||
|
import numpy as np
|
||||||
|
import rerun as rr
|
||||||
|
|
||||||
|
recording = rr.RecordingStream(
|
||||||
|
"nodedc_mission_core_recorded",
|
||||||
|
recording_id=recording_id,
|
||||||
|
)
|
||||||
|
stream = rr.binary_stream(recording)
|
||||||
|
try:
|
||||||
|
for timestamp, label in (
|
||||||
|
(timeline_start_ns, "LAB window start"),
|
||||||
|
(timeline_end_ns, "LAB window end"),
|
||||||
|
):
|
||||||
|
recording.set_time(
|
||||||
|
"session_time",
|
||||||
|
duration=np.timedelta64(timestamp, "ns"),
|
||||||
|
)
|
||||||
|
recording.log(
|
||||||
|
"/__mission_core/lab_window",
|
||||||
|
rr.TextDocument(label),
|
||||||
|
)
|
||||||
|
payload = stream.read(flush=True, flush_timeout_sec=5.0)
|
||||||
|
finally:
|
||||||
|
recording.disconnect()
|
||||||
|
if payload is None or not payload.startswith(b"RRF2"):
|
||||||
|
raise SessionIntegrityError("LAB replay window markers are invalid")
|
||||||
|
path.write_bytes(payload)
|
||||||
|
os.chmod(path, 0o600)
|
||||||
|
|
||||||
|
|
||||||
|
def _rerun_cli() -> Path:
|
||||||
|
adjacent = Path(sys.executable).with_name("rerun")
|
||||||
|
candidate = adjacent if adjacent.is_file() else None
|
||||||
|
if candidate is None:
|
||||||
|
resolved = shutil.which("rerun")
|
||||||
|
candidate = Path(resolved) if resolved is not None else None
|
||||||
|
if candidate is None or not candidate.is_file() or not os.access(candidate, os.X_OK):
|
||||||
|
raise SessionIntegrityError("Rerun CLI is unavailable for bounded LAB replay")
|
||||||
|
return candidate.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def _run_rerun(executable: Path, *arguments: str) -> None:
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(
|
||||||
|
[str(executable), *arguments],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||||
|
raise SessionIntegrityError("bounded LAB replay command failed") from exc
|
||||||
|
if completed.returncode != 0:
|
||||||
|
detail = completed.stderr.strip()[-1000:]
|
||||||
|
raise SessionIntegrityError(f"bounded LAB replay command failed: {detail}")
|
||||||
|
|
||||||
|
|
||||||
|
def _rrd_duration(value_ns: int) -> str:
|
||||||
|
for suffix, unit in (
|
||||||
|
("s", 1_000_000_000),
|
||||||
|
("ms", 1_000_000),
|
||||||
|
("us", 1_000),
|
||||||
|
("ns", 1),
|
||||||
|
):
|
||||||
|
if value_ns < unit:
|
||||||
|
continue
|
||||||
|
whole, remainder = divmod(value_ns, unit)
|
||||||
|
if remainder == 0:
|
||||||
|
return f"{whole}{suffix}"
|
||||||
|
digits = len(str(unit)) - 1
|
||||||
|
fraction = f"{remainder:0{digits}d}".rstrip("0")
|
||||||
|
return f"{whole}_{fraction}{suffix}"
|
||||||
|
return "0ns"
|
||||||
|
|
||||||
|
|
||||||
|
def _require_matching_bounded_cache(
|
||||||
|
target_rrd: Path,
|
||||||
|
target_sidecar: Path,
|
||||||
|
*,
|
||||||
|
lab_session_id: str,
|
||||||
|
timeline_start_ns: int,
|
||||||
|
timeline_end_ns: int,
|
||||||
|
) -> None:
|
||||||
|
target_root = target_rrd.parent
|
||||||
|
metadata = _require_regular(target_rrd, target_root)
|
||||||
|
_require_regular(target_sidecar, target_root)
|
||||||
|
document = _read_object(target_sidecar)
|
||||||
|
if (
|
||||||
|
document.get("session_id") != lab_session_id
|
||||||
|
or document.get("recording_byte_length") != metadata.st_size
|
||||||
|
or document.get("recording_mtime_ns") != metadata.st_mtime_ns
|
||||||
|
or document.get("recording_sha256") != _sha256(target_rrd)
|
||||||
|
or document.get("timeline_start_ns") != timeline_start_ns
|
||||||
|
or document.get("timeline_end_ns") != timeline_end_ns
|
||||||
|
):
|
||||||
|
raise SessionIntegrityError("LAB recording cache already contains different bytes")
|
||||||
|
|
||||||
|
|
||||||
def _clone_recorded_media_sidecars(
|
def _clone_recorded_media_sidecars(
|
||||||
root: Path,
|
root: Path,
|
||||||
*,
|
*,
|
||||||
|
|
@ -121,6 +349,14 @@ def _canonical_json(value: object) -> bytes:
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def _read_object(path: Path) -> dict[str, Any]:
|
def _read_object(path: Path) -> dict[str, Any]:
|
||||||
value = json.loads(path.read_text(encoding="utf-8"))
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
|
@ -333,6 +334,8 @@ class SessionStore:
|
||||||
source_result_id: str | None = None,
|
source_result_id: str | None = None,
|
||||||
config_sha256: str | None = None,
|
config_sha256: str | None = None,
|
||||||
provenance: dict[str, Any] | None = None,
|
provenance: dict[str, Any] | None = None,
|
||||||
|
duration_seconds: float | None = None,
|
||||||
|
include_recorded_media: bool = True,
|
||||||
) -> LabSessionBinding:
|
) -> LabSessionBinding:
|
||||||
"""Append one immutable catalog projection over an existing source session.
|
"""Append one immutable catalog projection over an existing source session.
|
||||||
|
|
||||||
|
|
@ -357,6 +360,12 @@ class SessionStore:
|
||||||
if not 1 <= len(normalized_name) <= 160:
|
if not 1 <= len(normalized_name) <= 160:
|
||||||
raise ValueError("LAB display name must contain 1..160 characters")
|
raise ValueError("LAB display name must contain 1..160 characters")
|
||||||
_require_utc_timestamp(run_created_at_utc, "LAB run creation time")
|
_require_utc_timestamp(run_created_at_utc, "LAB run creation time")
|
||||||
|
if duration_seconds is not None and (
|
||||||
|
isinstance(duration_seconds, bool)
|
||||||
|
or not math.isfinite(duration_seconds)
|
||||||
|
or duration_seconds <= 0
|
||||||
|
):
|
||||||
|
raise ValueError("LAB duration must be a positive finite value")
|
||||||
serialized_provenance = _serialize_provenance(provenance or {})
|
serialized_provenance = _serialize_provenance(provenance or {})
|
||||||
published_at = utc_now_iso()
|
published_at = utc_now_iso()
|
||||||
|
|
||||||
|
|
@ -428,7 +437,11 @@ class SessionStore:
|
||||||
source["status"],
|
source["status"],
|
||||||
run_created_at_utc,
|
run_created_at_utc,
|
||||||
run_created_at_utc,
|
run_created_at_utc,
|
||||||
source["duration_seconds"],
|
(
|
||||||
|
source["duration_seconds"]
|
||||||
|
if duration_seconds is None
|
||||||
|
else duration_seconds
|
||||||
|
),
|
||||||
source["modalities_json"],
|
source["modalities_json"],
|
||||||
source["replayable"],
|
source["replayable"],
|
||||||
LAB_ORIGIN,
|
LAB_ORIGIN,
|
||||||
|
|
@ -449,8 +462,9 @@ class SessionStore:
|
||||||
"integrity_status, locator, replay_byte_length) "
|
"integrity_status, locator, replay_byte_length) "
|
||||||
"SELECT ?, artifact_id, kind, media_type, byte_length, sha256, "
|
"SELECT ?, artifact_id, kind, media_type, byte_length, sha256, "
|
||||||
"integrity_status, locator, replay_byte_length "
|
"integrity_status, locator, replay_byte_length "
|
||||||
"FROM observation_session_artifacts WHERE session_id = ?",
|
"FROM observation_session_artifacts WHERE session_id = ? "
|
||||||
(session_id, source_session_id),
|
"AND (? OR kind <> 'recorded-video')",
|
||||||
|
(session_id, source_session_id, include_recorded_media),
|
||||||
)
|
)
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"INSERT INTO observation_session_sources "
|
"INSERT INTO observation_session_sources "
|
||||||
|
|
@ -458,8 +472,8 @@ class SessionStore:
|
||||||
"seekable, artifact_id) "
|
"seekable, artifact_id) "
|
||||||
"SELECT ?, source_id, semantic_channel_id, modality, status, "
|
"SELECT ?, source_id, semantic_channel_id, modality, status, "
|
||||||
"seekable, artifact_id FROM observation_session_sources "
|
"seekable, artifact_id FROM observation_session_sources "
|
||||||
"WHERE session_id = ?",
|
"WHERE session_id = ? AND (? OR modality <> 'video')",
|
||||||
(session_id, source_session_id),
|
(session_id, source_session_id, include_recorded_media),
|
||||||
)
|
)
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"INSERT INTO observation_lab_instances "
|
"INSERT INTO observation_lab_instances "
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ def test_publish_lab_replay_cache_hardlinks_rrd_and_rebinds_sidecars(
|
||||||
"recording_byte_length": source_rrd.stat().st_size,
|
"recording_byte_length": source_rrd.stat().st_size,
|
||||||
"recording_mtime_ns": source_rrd.stat().st_mtime_ns,
|
"recording_mtime_ns": source_rrd.stat().st_mtime_ns,
|
||||||
"recording_sha256": hashlib.sha256(source_rrd.read_bytes()).hexdigest(),
|
"recording_sha256": hashlib.sha256(source_rrd.read_bytes()).hexdigest(),
|
||||||
|
"timeline": "session_time",
|
||||||
|
"timeline_start_ns": 0,
|
||||||
|
"timeline_end_ns": 1,
|
||||||
}
|
}
|
||||||
(source / RECORDING_CACHE_SIDECAR_FILENAME).write_text(
|
(source / RECORDING_CACHE_SIDECAR_FILENAME).write_text(
|
||||||
json.dumps(source_sidecar),
|
json.dumps(source_sidecar),
|
||||||
|
|
@ -87,3 +90,81 @@ def test_publish_lab_replay_cache_hardlinks_rrd_and_rebinds_sidecars(
|
||||||
|
|
||||||
lab_rrd.unlink()
|
lab_rrd.unlink()
|
||||||
assert source_rrd.read_bytes() == b"RRF2immutable"
|
assert source_rrd.read_bytes() == b"RRF2immutable"
|
||||||
|
|
||||||
|
|
||||||
|
def test_publish_lab_replay_cache_materializes_exact_bounded_window(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
import numpy as np
|
||||||
|
import rerun as rr
|
||||||
|
|
||||||
|
data = tmp_path / "mission-core"
|
||||||
|
recordings = data / "recordings"
|
||||||
|
source = recordings / "source-session"
|
||||||
|
source.mkdir(parents=True)
|
||||||
|
source_rrd = source / RECORDING_CACHE_FILENAME
|
||||||
|
recording = rr.RecordingStream(
|
||||||
|
"nodedc_mission_core_recorded",
|
||||||
|
recording_id=source.name,
|
||||||
|
)
|
||||||
|
stream = rr.binary_stream(recording)
|
||||||
|
try:
|
||||||
|
for index, timestamp in enumerate((0, 2_000_000_000, 4_000_000_000, 8_000_000_000)):
|
||||||
|
recording.set_time(
|
||||||
|
"session_time",
|
||||||
|
duration=np.timedelta64(timestamp, "ns"),
|
||||||
|
)
|
||||||
|
recording.log(
|
||||||
|
"/world/points",
|
||||||
|
rr.Points3D([[float(index), 0.0, 0.0]]),
|
||||||
|
)
|
||||||
|
payload = stream.read(flush=True, flush_timeout_sec=5.0)
|
||||||
|
finally:
|
||||||
|
recording.disconnect()
|
||||||
|
assert payload is not None
|
||||||
|
source_rrd.write_bytes(payload)
|
||||||
|
source_sidecar = {
|
||||||
|
"schema_version": CACHE_SCHEMA,
|
||||||
|
"session_id": source.name,
|
||||||
|
"recording_byte_length": source_rrd.stat().st_size,
|
||||||
|
"recording_mtime_ns": source_rrd.stat().st_mtime_ns,
|
||||||
|
"recording_sha256": hashlib.sha256(source_rrd.read_bytes()).hexdigest(),
|
||||||
|
"timeline": "session_time",
|
||||||
|
"timeline_start_ns": 0,
|
||||||
|
"timeline_end_ns": 8_000_000_000,
|
||||||
|
}
|
||||||
|
(source / RECORDING_CACHE_SIDECAR_FILENAME).write_text(
|
||||||
|
json.dumps(source_sidecar),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
publish_lab_replay_cache(
|
||||||
|
data,
|
||||||
|
source_session_id=source.name,
|
||||||
|
lab_session_id="lab-e21-bounded",
|
||||||
|
timeline_start_ns=2_000_000_000,
|
||||||
|
timeline_end_ns=4_000_000_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
lab = recordings / "lab-e21-bounded"
|
||||||
|
lab_rrd = lab / RECORDING_CACHE_FILENAME
|
||||||
|
assert lab_rrd.stat().st_ino != source_rrd.stat().st_ino
|
||||||
|
bounded = json.loads(
|
||||||
|
(lab / RECORDING_CACHE_SIDECAR_FILENAME).read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
assert bounded["timeline_start_ns"] == 2_000_000_000
|
||||||
|
assert bounded["timeline_end_ns"] == 4_000_000_000
|
||||||
|
assert bounded["recording_byte_length"] == lab_rrd.stat().st_size
|
||||||
|
assert bounded["recording_sha256"] == hashlib.sha256(lab_rrd.read_bytes()).hexdigest()
|
||||||
|
assert "lab_window" not in bounded
|
||||||
|
|
||||||
|
original_stat = lab_rrd.stat()
|
||||||
|
publish_lab_replay_cache(
|
||||||
|
data,
|
||||||
|
source_session_id=source.name,
|
||||||
|
lab_session_id="lab-e21-bounded",
|
||||||
|
timeline_start_ns=2_000_000_000,
|
||||||
|
timeline_end_ns=4_000_000_000,
|
||||||
|
)
|
||||||
|
assert lab_rrd.stat().st_ino == original_stat.st_ino
|
||||||
|
assert lab_rrd.stat().st_mtime_ns == original_stat.st_mtime_ns
|
||||||
|
|
|
||||||
|
|
@ -849,6 +849,38 @@ def test_lab_instance_publication_is_idempotent_but_provenance_is_immutable(
|
||||||
store.publish_lab_instance(**{**parameters, "config_sha256": "0" * 64})
|
store.publish_lab_instance(**{**parameters, "config_sha256": "0" * 64})
|
||||||
|
|
||||||
|
|
||||||
|
def test_bounded_lab_instance_excludes_unbounded_recorded_media(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
repository = tmp_path / "repo"
|
||||||
|
sessions = repository / "sessions"
|
||||||
|
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||||
|
make_recorded_camera_source(source)
|
||||||
|
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||||
|
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||||
|
assert store.list_recorded_media(source.name)
|
||||||
|
|
||||||
|
binding = store.publish_lab_instance(
|
||||||
|
session_id="lab-e21-window-d0201712",
|
||||||
|
source_session_id=source.name,
|
||||||
|
display_name="LAB E21.2 · bounded 60s",
|
||||||
|
lab_id="LAB E21.2",
|
||||||
|
result_kind="e21-realtime-envelope",
|
||||||
|
result_id="e10-integrated-perception-" + "1" * 64,
|
||||||
|
source_result_id="e21-realtime-envelope-" + "2" * 64,
|
||||||
|
config_sha256="3" * 64,
|
||||||
|
run_created_at_utc="2026-07-23T15:55:15.548Z",
|
||||||
|
duration_seconds=59.962,
|
||||||
|
include_recorded_media=False,
|
||||||
|
provenance={"timeline_scope": "bounded"},
|
||||||
|
)
|
||||||
|
|
||||||
|
detail = store.get_session(binding.session_id)
|
||||||
|
assert detail.summary.duration_seconds == pytest.approx(59.962)
|
||||||
|
assert store.list_recorded_media(binding.session_id) == ()
|
||||||
|
assert store.prepare_replay(binding.session_id).primary_artifact.path.is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_delete_session_rejects_a_catalog_target_outside_its_allowed_root(
|
def test_delete_session_rejects_a_catalog_target_outside_its_allowed_root(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue