fix(perception): bound LAB replay to AI window

This commit is contained in:
DCCONSTRUCTIONS 2026-07-23 21:15:53 +03:00
parent 10019a454a
commit 082b7057da
7 changed files with 429 additions and 38 deletions

View File

@ -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.

View File

@ -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, 239242, 433, 498 | | Explicit latest-wins replacement frames | 12, 239242, 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.42185729295.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

View File

@ -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,
}, },
) )

View File

@ -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):

View File

@ -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 "

View File

@ -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

View File

@ -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: