30 KiB
Observation sessions, playback and workspace layout
Status: implemented for native K1 point/pose evidence and host-side camera
archival. Valid K1 ModelingReport scan time, distance and speed are also
materialized as recorded Rerun time series. Recorded spatial playback is exposed
in the Mission Core observation workspace. The camera archive/player contract
is implemented and covered by tests, but all retained real K1 sessions predate
canonical camera archival. Physical shared-timeline playback of one selected
camera plus point cloud therefore remains an open acceptance gate; historical
sessions contain no recoverable video.
Operator path
- Enter the required project name and start a normal acquisition from Парк → Локальное устройство. The NFKC-normalized/trimmed value is local display metadata, not a filesystem path, and becomes the saved-session catalog name.
- Open Наблюдение → Пространственная сцена. Live point cloud, trajectory and the selected camera remain live-only while acquisition is running.
- Stop acquisition normally, or allow the local service to recover an unexpected interruption on its next start. Session recording is automatic; the disk action is not required.
- Open Сохранённые сессии in the observation header. The menu shows the three most recent indexed runs, their date, duration, state, modalities and background preparation state.
- Choose a replayable run. Opening a replay never performs conversion in the request. A ready recording opens immediately; otherwise the client receives HTTP 202, keeps the current scene mounted and polls the preparation status. Only after the server returns a verified launch descriptor does Mission Core pause and unload the previous viewer.
- The client then receives and decodes the complete RRD. The new viewport and
timeline stay hidden until the declared recording range is fully buffered
(
fullyBuffered). Playback starts once at that point. Partial frames are never shown. If the run contains canonical camera archives, its recorded camera sources replace all live device overlays. - Use Воспроизвести / Пауза, the scrubber, К началу and К концу on
the right half of the bottom timeline. The left half changes point-cloud
accumulation. The recorded timeline is zero-based
session_time. - Use the first disk button to save the current workspace layout. The next opening restores display settings and dynamic sensor-source window positions. The transient Движок / Слои / Отображение tool windows are scoped to the mounted spatial workspace and are never persisted. This action saves no sensor evidence.
Mission Core does not switch an active acquisition to another spatial source. Every nonterminal or unknown acquisition state fail-closes saved-session replay, persisted replay reattach and manual RRD/Rerun input/apply/reset with Завершите текущий приём перед сменой источника. A plugin-owned automatic source action clears the older source and opens the scene only after its start returns success; failure leaves the current scene mounted. This internal success-result transition is not an operator switch. The guard does not stop the scanner or local receiver automatically.
Display controls have no separate Apply/Reset transaction. Boolean controls commit immediately; sliders, colors and selects commit on release/blur or after a short quiet period, and concurrent commits collapse to the newest value. A layout save flushes and awaits this queue before serializing the confirmed settings. Recorded display updates reuse stable Rerun scene identifiers and do not mutate playback state, so changing accumulation, size, color or visibility does not pause the active replay or replace its camera view.
Completed and recovered sessions are discovered on startup and by the catalog
reconciler. The first scan is only a historical baseline: it never enqueues the
existing archive. A session that becomes finalized after that baseline is
deduplicated into the bounded, single-worker preparation queue. The worker
validates the native source, exports point/pose data, verifies source stability
and both SHA-256 digests, finalizes once, and atomically publishes the
digest-bound derived RRD plus its cache sidecar. Before the same job becomes
ready, it also parses and validates every archived camera index and publishes
the immutable media-manifest sidecar. The catalog exposes preparation state and
progress while this runs; a recording URL and camera descriptors are returned
only after the complete launch generation has passed validation.
Preparation is a backend lifecycle, not a viewer computation. Reopening, seeking, refreshing the catalog or changing the workspace never reruns a published conversion. After a process restart, a catalog/replay read restores a complete compatible RRD and camera package from its durable sidecars using bounded schema and stat-identity checks; it does not hash native evidence, parse camera indexes or invoke the exporter. A historical session whose package is missing, stale or from an incompatible cache schema remains cold. Only an explicit replay/RRD request schedules that one session for preparation; listing the catalog and finalizing another session do not schedule it.
The session menu uses three operator indicators:
| Indicator | Catalog/preparation state | Meaning |
|---|---|---|
Solid green + Готово |
ready |
Verified RRD is available. |
Pulsing green + Обработка |
queued, validating, exporting, finalizing |
Background preparation is active. |
Solid gray + Подготовить |
replayable session with no compatible published package | No work is running; choosing the row schedules this session only. |
Dim gray + Ошибка |
failed, cancelled, non-replayable or invalid session | No launchable recording; inspect the message or retry. |
Saved-session status never uses yellow or red. Switching sessions aborts only the obsolete browser poll; it does not cancel the process-owned conversion. A queued job may legitimately wait behind another export and therefore uses the overall preparation deadline rather than the active-export heartbeat timeout.
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.
Later openings reuse a verified, digest-bound cache. Derived RRD cache v9 is
intentionally incompatible with v8 and older generations. In addition to plugin
and ordered-artifact identity, it binds the durable clock origin and active
envelope and materializes real session_time = 0 origin and sealed completion
rows. Older generations remain cold after startup and are rebuilt only when an
operator explicitly opens that session.
The browser may use the strict source_url with If-Match, while the embedded
Rerun loader uses the canonical viewer_source_url whose lowercase SHA-256
generation query is bound to the same launch descriptor. A missing or stale
generation fails with 412; a matching URL is served with exact length, strong
ETag and private immutable/no-transform caching. Every cache pin is released when the
HTTP response completes, disconnects or fails, so an interrupted switch cannot
make a derived recording permanently non-evictable. A bounded 120-second launch
reservation also protects the artifact between the verified launch response and
the WebViewer's subsequent RRD request, including a cold WASM startup; it
expires automatically if that request never arrives.
Storage roots
By default, both catalog state and new source evidence are private to the checkout:
.runtime/mission-core/
├── mission-core.sqlite3
├── mission-core.sqlite3-shm
├── mission-core.sqlite3-wal
├── evidence/
│ └── sessions/
│ ├── .current_session # present only while a writer owns the root
│ └── <UTC>_viewer_live/
│ ├── manifest.redacted.json # normalized local project display metadata
│ ├── captures/
│ │ └── mqtt_live/
│ │ ├── mqtt.raw.k1mqtt
│ │ ├── mqtt.metadata.jsonl
│ │ ├── mqtt.timeline.origin.json
│ │ ├── mqtt.timeline.json # provisional transport envelope
│ │ ├── mqtt.timeline.session-<sha256>.json # sealed generation
│ │ └── mqtt.summary.json # atomic active-envelope pointer
│ └── media/ # canonical camera archives
└── recordings/
├── .export.lock # cross-process conversion/eviction lock
└── <opaque-session-id>/
├── scene.rrd
└── scene.rrd.cache.json
MISSIONCORE_DATA_DIR relocates the catalog and derived cache. Unless it is
overridden separately, new evidence is written to
$MISSIONCORE_DATA_DIR/evidence/sessions. MISSIONCORE_EVIDENCE_DIR can select
another absolute MQTT evidence root. In the current K1 integration the camera
gateway additionally confines its recording directory to the repository
checkout, so a full point-plus-camera acquisition must keep the evidence root
inside that checkout. An external evidence volume currently supports MQTT
capture/cataloging but camera archival will fail closed until the gateway gets
a separately attested storage root:
export MISSIONCORE_DATA_DIR=/absolute/private/path/mission-core
# Full K1 point-plus-camera evidence must currently remain below the checkout.
export MISSIONCORE_EVIDENCE_DIR=/absolute/path/to/NODEDC_MISSION_CORE/.runtime/mission-core/evidence/sessions
export MISSIONCORE_RRD_CACHE_MAX_BYTES=8589934592
export MISSIONCORE_RRD_FREE_SPACE_RESERVE_BYTES=2147483648
uv run k1link serve
The directories and derived recording cache use owner-only permissions where
the host filesystem permits them. A new acquisition writer is assigned only a
direct child of the private evidence root; it no longer writes a new run to the
repository-level sessions/ directory.
Repository-level sessions/*_viewer_live runs are a legacy, import-only source
for the catalog. They are discovered and confined in place: refresh does not
copy, move or migrate their large payloads, and no current writer is assigned
that root. Startup may recovery-seal an already existing, incomplete canonical
camera epoch there by preserving its valid segment prefix and writing an
interrupted summary; this is evidence recovery, not a new acquisition write.
Never add .runtime/, sessions/, raw captures, RRD files or camera media to
Git. They can contain mapped interiors, trajectories and identifiable images.
Session source of record
For the current K1 profile:
MQTT callback
├─ durable native .k1mqtt + aligned metadata (source of record)
├─ durable clock origin (before camera production)
├─ provisional + content-addressed envelopes (transport/session bounds)
├─ ModelingReport -> live product metrics (before visual preview queue)
└─ bounded latest-wins Rerun live preview (disposable)
selected RTSP producer
├─ durable init + fMP4 segments + JSONL index (camera evidence)
└─ bounded WebSocket/MSE preview (disposable)
The live preview is intentionally allowed to drop frames under load. Native point/pose evidence and camera archive writes do not traverse that queue.
Native .k1mqtt bytes with aligned metadata, the capture-clock artifacts and
canonical camera fMP4 archives are the evidence source of truth. The derived RRD
contains every decodable point and pose frame plus every valid ModelingReport
distance/speed/scan-time sample from that source, but remains a rebuildable view
rather than an evidence master. A cache entry is eligible for rebuild only when
native source identity/digests change or an incompatible derived-data export
revision is introduced. Historical entries are not rebuilt during startup or
because another session is finalized; an explicit open schedules the affected
entry. A UI blueprint or workspace-layout revision never invalidates or rewrites
the data RRD. Cache v8 and older payloads are not reusable as v9 because they do
not bind both real capture-envelope endpoints.
ModelingReport time is the device's ScanTime counter at two ticks per second;
distance and speed are the reported MoveDistance/MoveSpeed values. Live state
keeps the current device scan generation and does not accumulate an earlier
distance after the device resets scan time and route distance. These values are
not reconstructed from pose integration or a browser timer.
Expensive cache misses run through the bounded preparation worker and one global cross-process export gate to cap peak RAM, CPU and temporary-disk use. Crash leftovers from candidates, exporter temporary files and staged replay prefixes are scavenged under that lock before quota accounting. Ready cache hits and active response leases do not wait behind that gate. The derived cache has an 8 GiB default quota, preserves a 2 GiB default filesystem reserve and evicts least-recently-used RRDs only; it never deletes native evidence.
Capture-clock envelope
New K1 captures first publish bounded schema-1
captures/mqtt_live/mqtt.timeline.origin.json with start epoch and monotonic
nanoseconds. The writer creates and synchronizes this immutable artifact after
its raw/metadata files are open and before camera production is armed. A crash
therefore cannot leave retained camera evidence whose intended session zero
existed only in memory.
MQTT shutdown then publishes schema-1 mqtt.timeline.json with the same start
plus transport completion. Capture-summary schema 2 initially points to it with
capture_clock_scope: transport, and binds both origin/envelope names and
SHA-256 values. That provisional file is not rewritten.
After the camera archive and MQTT/runtime stop, the acquisition owner creates
mqtt.timeline.session-<sha256>.json with the extended session completion and
atomically switches mqtt.summary.json to that content-addressed name, digest
and capture_clock_scope: session while still holding the active-session lease.
Publication order is sealed file first, summary pointer second, so a crash leaves
the previous pointer valid and the idempotent seal can converge on retry. The
same summary update records session_elapsed_seconds. Files and containing
directories are synchronized at their publication boundaries.
Discovery validates the summary-selected filename, both digests, the shared origin and the envelope bounds, then catalogs that exact artifact. The materializer passes its exact staged path to the exporter; it never chooses a sealed generation by glob or “latest file” ordering. Direct/legacy export keeps the fixed provisional-name fallback only for evidence outside the cataloged session contract, and orphan sealed candidates not selected by the validated summary are ignored.
A provisional transport scope is never advertised as combined camera replay.
An interrupted origin-only point/pose prefix may use the durable origin and its
last validated message as a compatibility end, but it cannot advertise camera
media. If the origin, active envelope, summary pointer or digest relationship is
missing/corrupt, new-schema discovery fails closed instead of inventing a
session boundary.
For a sealed generation, session_time = 0 is the envelope start and the RRD
end is the envelope completion. Cache v9 logs real rows at
/__mission_core/session_origin and /__mission_core/session_end, so a camera
fragment accepted before the first MQTT message or after the last point/pose
message can remain inside the declared RRD interval. Valid legacy sessions
without this artifact keep their first/last-message compatibility fallback and
cannot gain historical camera coverage.
Camera archive contract
New acquisitions archive each selected source and codec epoch below the same private evidence session:
<evidence-root>/sessions/<session-id>/media/<stable-source-id>/epoch-1/
├── init.mp4
├── segments/
│ ├── 1.m4s
│ └── ...
├── index.jsonl
└── summary.json
Each index row binds a segment sequence, byte length, SHA-256 and host arrival
epoch/monotonic timestamps. Camera durability uses a
per-segment-fsync policy: init.mp4 and every complete media segment are
individually fsynced and their directory entries synchronized before the
matching JSONL index row is committed. The index and an atomic interrupted
checkpoint summary are fsynced before the append returns. The old interval and
byte constructor options are compatibility-only and cannot weaken this
segment-bound RPO. A power failure may still lose or leave uncommitted the
fragment currently being produced; it cannot make a committed index row point
past durable media. Camera windows may close or reconnect without terminating
archival while the owning acquisition remains active.
Synchronization is host-arrival-best-effort: LiDAR/MQTT and camera segments
share the Mac host clock boundary, but K1 sensor exposure time and LiDAR firing
time are not proven to use a shared device clock. Do not infer frame-accurate
calibration from the playback timeline.
Finalized media is prepared once by the same process-owned background job that
materializes the RRD. The gateway records host time only after a complete
moof+mdat fragment has arrived, so that timestamp is an availability/end
anchor, never a fragment-start timestamp. Preparation reads and SHA-verifies
every fragment, parses bounded ISO-BMFF timing tables (mdhd, trex, tfhd,
trun), anchors the epoch at
max(0, first_arrival - first_fragment_duration), and sets its end to that start
plus the checked sum of every decoded fragment duration. The declared interval
therefore has exactly the duration MSE is expected to expose and is not stretched
by host scheduling jitter.
Epoch arrivals must be strictly monotonic; epoch intervals must be finite,
monotonic and non-overlapping. Replay v2 also requires every media interval to
fit inside the spatial RRD interval with a 50 ms numeric tolerance; it fails
preparation instead of clamping unreachable evidence. A future replay v3 must
separate spatial_range from a session-wide union range before out-of-RRD camera
coverage can be navigated. A missing, ambiguous, oversized or otherwise
unparseable timing table fails preparation; the archive remains evidence but is
never advertised as seekable media.
The prepared path-free v2 descriptor and full source stat identity (native raw
and timing metadata plus every camera summary, index, init and segment file) are
written under the private derived cache with a schema, generation and checksum.
Publication uses a private temporary file, file and directory fsync, and atomic
rename; startup scavenges crash-left temporary files. A restart reuses this
sidecar after confined O(n) stat validation, without rereading, hashing or parsing
media. A missing, corrupt or stale sidecar remains cold until an explicit open
schedules the complete session package in the background worker. No catalog,
status, manifest or payload request itself performs conversion or recalculates
these intervals.
Every derived RRD contains a real session_time = 0 row at the internal
/__mission_core/session_origin entity. The anchor is deliberately outside
/world, so it establishes the actual recording time range without creating a
3D layer or drawable scene object. Preparation verifies the derived recording
against the declared spatial range rather than relying on summary metadata
alone.
Sessions made before this archive contract have no recoverable video even if a camera preview was visible at the time. In particular, the 2026-07-16 browser camera acceptance run retained point/pose evidence only.
Crash and interruption behavior
- SQLite uses WAL, foreign keys and full synchronous durability.
- Before creating a session directory, the acquisition takes an exclusive,
cross-process lease by atomically creating and locking
<evidence-root>/.current_session. The marker contains only the direct child session name. While the lock is held, a second writer fails closed and discovery excludes that in-progress session from replay. - A process exit releases the operating-system lock. On the next service start, stale-marker recovery removes the marker only after it can take the lock and revalidate the marker inode without following symlinks. It never deletes the interrupted session directory; normal catalog recovery then evaluates the durable prefix. A locked/live or suspicious marker is left untouched.
- Native MQTT writes use a bounded group commit: at most 0.5 seconds, 4 MiB or 32 messages per group. Raw bytes are fsynced before their metadata rows are written and fsynced. This is a bounded RPO, not a zero-loss guarantee: a hard process or power failure may discard the final uncommitted group. Raw bytes from an interrupted commit window may survive beyond the durable metadata; replay uses only the last validated metadata-aligned raw boundary.
- New capture-summary schema 2 binds the durable origin and its active envelope
by name and SHA-256. A normal combined replay requires the owner-sealed,
content-addressed
sessionscope. A crash can leave an origin-only or provisionaltransportgeneration, but discovery will not use either to advertise camera coverage. A mismatched origin/envelope/summary fails closed. - A native capture with a missing final summary is accepted only when the raw
and metadata prefix is bounded, aligned and structurally valid. It is cataloged
as
interrupted, never silently promoted toready. - A non-newline metadata crash tail can be ignored. Newline-terminated or mid-file corruption fails closed.
- Camera recovery retains a contiguous valid segment prefix, quarantines
non-contiguous/orphan fragments instead of deleting them, rebuilds the index
when necessary and writes an
interruptedsummary. Unindexed bytes are not presented as valid media. - If a valid normal summary appears later, repeat discovery updates the same
session to
ready. - Materialization detects a native capture changing during export and refuses to publish the derived RRD. It reopens the prepared raw source with no symlink following inside the cataloged session roots, and interrupted recordings are materialized from the last validated raw/metadata boundary only.
- Internal preparation cancellation is cooperative. A queued job cancels immediately; an active job stops at a safe checkpoint, removes its candidate and never publishes a partial RRD. The operator UI does not cancel automatic preparation when switching sessions or closing a tab: preparation belongs to the application worker, not to an HTTP request.
failedandcancelledare terminal, retryable states. The API exposes a sanitized error, and an explicit retry creates a fresh job for the current source identity. A stalled poll or failed switch leaves the current scene mounted; the latest operator selection wins over obsolete responses.- A lifecycle shutdown marks only its own interrupted work for automatic reconciliation after restart. An operator cancellation and a genuine failed export remain terminal and are never retried forever by the reconciler.
These rules provide crash recovery, not replication. A single host disk failure can still destroy local data. Vehicle deployment must add independent onboard and control-station copies, capacity monitoring and a documented retention policy.
HTTP API
All paths are same-origin and expose opaque identifiers only:
GET /api/v1/observation-sessions?limit=3
GET /api/v1/observation-sessions/{id}
POST /api/v1/observation-sessions/{id}/replay
GET /api/v1/observation-sessions/{id}/recording-preparation
DELETE /api/v1/observation-sessions/{id}/recording-preparation
GET /api/v1/observation-sessions/{id}/recording.rrd
POST /api/v1/observation-sessions/{id}/blueprint.rrd
GET /api/v1/observation-sessions/{id}/media/{artifact}/manifest
GET /api/v1/observation-sessions/{id}/media/{artifact}/epochs/{n}/init.mp4
GET /api/v1/observation-sessions/{id}/media/{artifact}/epochs/{n}/segments/{m}.m4s
GET /api/v1/workspace-layouts/observation.spatial
PUT /api/v1/workspace-layouts/observation.spatial
The catalog embeds each replayable session's preparation state and progress.
POST .../replay returns either a verified replay v2 launch document or HTTP
202 with the preparation v1 document, Location, Retry-After, an exact quoted
preparation ETag and a same-origin status URL. Polling
GET .../recording-preparation sends that value as If-Match and returns HTTP
202 while the job is active, HTTP 409 for retryable failed/cancelled states,
and the launch document only when the same job and artifact are ready; every
response echoes the same ETag. DELETE also requires If-Match, so a stale
tab cannot cancel a replacement job. Conversion is never executed synchronously
by a replay-open request. Playback speed and loop are request-local launch
policy; they are not stored on or shared through a preparation job.
Production recording.rrd access is bound to the launch generation. The client
can use the strict query-free URL with exact strong
If-Match: "sha256:<launch.sha256>"; a request with neither an If-Match nor a
generation returns 428. The embedded Rerun receiver instead gets only the
canonical viewer_source_url = source_url + "?generation=<launch.sha256>".
The server returns 412 for a malformed or replaced generation before opening
the body. A successful response echoes the strong ETag, exact Content-Length,
application/vnd.rerun.rrd and immutable private/no-transform cache policy.
Rerun's native HTTP receiver owns incremental decoding; LogChannel.send_rrd
is reserved for independently complete RRD payloads such as the small generated
blueprint and must never receive arbitrary HTTP byte slices. The canvas,
timeline, controller and autoplay remain closed until the decoded spatial range
exactly matches the launch descriptor and every declared camera is ready. API
documents never contain local paths. Layout updates require the quoted current
revision in If-Match; stale writers receive HTTP 412 rather than overwriting
another saved profile.
Recorded media routes expose only opaque catalog identifiers and ordinal codec
epochs. The required missioncore.observation-recorded-media/v2 manifest carries
a strong generation_sha256, exact JS-safe aggregate byte_length, and finite
timeline_start_seconds / timeline_end_seconds for every epoch. Epoch ends
participate in the generation digest. Every epoch also declares its init byte
length/digest and a complete contiguous segment list with sequence, URL, byte
length and digest. The launch source repeats the same aggregate byte_length
and uses exactly max(epoch.timeline_end_seconds) as its end; the spatial RRD
end must never pad camera coverage. The browser cross-checks launch, manifest and
component lengths. The current browser laboratory policy accepts at most 16
camera sources, 128 MiB per source and 512 MiB across the session. This launch
preflight finishes before the first camera manifest/init/segment GET; admitted
cameras are then downloaded and decode-probed one at a time. Verified raw
buffers remain immutable so route changes and player remounts cannot append
emptied data. This bounded in-memory strategy is deliberate for the laboratory
milestone; an OPFS-backed sealed-generation cache is the next scaling step for
larger rigs and must preserve the same launch/manifest/hash admission contract.
The manifest response ETag is the exact generation, and the manifest GET itself
requires that generation in If-Match. Init and media GETs likewise require the
descriptor's exact SHA If-Match, then open through a confined directory file
descriptor with no symlink following, verify the declared SHA-256 and serve
exact-length bytes with immutable private no-transform caching and byte-range
support. Physical source ids, RTSP addresses and storage paths never cross the
API boundary.
Current limit
The spatial RRD, trajectory, archived device-metric series and archived fMP4
cameras use the same operator scrubber now. Camera epochs are aligned to
zero-based session_time from the shared host-arrival monotonic clock and
rendered through MSE. The metric tab carries device-reported route distance,
speed and scan time at their ModelingReport receive times. The client selects
a camera epoch only inside its declared inclusive interval and verifies that the
decoded MSE seekable duration covers that interval. This remains best-effort
correlation: codec PTS, K1 sensor exposure time and LiDAR firing time are not
proven to share a device clock. A codec epoch whose init segment does not expose
a browser-supported codec or whose timing cannot be proven remains retained
evidence and fails closed in the UI/background preparation.
Historical sessions with no canonical camera archive honestly show no recorded
video. Recorded blueprints can change accumulation, grid visibility, point and
trajectory visibility, point radius and a uniform custom point color without
rewriting the recording. The client deliberately has no progressive recorded
mode: the viewport, timeline and autoplay gate remain closed until the complete
declared RRD has been received and decoded. Height, intensity, distance and RGB
palettes remain baked into current RRD rows; fully dynamic recoloring requires
exporting the corresponding scalar components in a future recording schema.
Verification boundary — 2026-07-17
The current automated gate covers project-name validation, source-switch
fail-closure, optional plugin scene controls, bounded ModelingReport decode,
inert start/stop encoding and correlation, capture-clock publication/validation,
cache-v9 origin/end materialization and archived metric series. The standard
repository gate remains Python tests/Ruff/mypy plus frontend unit tests,
TypeScript checking and the Vite production build; exact pass counts belong to
the commit's CI/pre-push result rather than this durable architecture contract.
Vite still reports its expected large-chunk warning for the embedded Rerun viewer/WASM payload. That is a packaging optimization item, not a failed gate. No retained physical K1 session contains the new canonical camera archive, so a real point-cloud plus one-camera recorded playback remains an explicit hardware acceptance test. Automated protocol tests also do not authorize K1 modeling publishing: private application-authority loading and durable save completion remain separate physical/security gates.