Skip to content

GLIM clean-room adapter Phase 3b runtime (2026-08)

Phase 3b is an additive, opt-in exact-core trajectory runtime vertical slice. It is not an active benchmark runner and does not consume bags, datasets, GT, scorer inputs, or active profiles. The Phase 0 host contract remains GLIM-free; exact GLIM headers and objects are confined to phase3b/ behind the GlimCoreSession::Impl boundary. The complete concrete mapping extension is documented separately in Phase 3c map output; neither phase makes any benchmark or SOTA claim eligible.

Immutable core and build surface

The runtime links only the preserved public install derived from GLIM faa264a1bce1bda406f73457e35511f56cdc2eaa at /tmp/competitive-rival-closure.sXE92h/glim and /tmp/glim-clean-room-phase1.mwzqdq6t/install/*. The target is docker/benchmark_adapters/glim_clean_room/phase3b/; it does not include, copy, or link any ROS bridge source. Exact-core configuration is an explicit directory containing config.json; there is no working-directory or source tree fallback. The CMake target requires the exact pinned GLIM vendored thirdparty/json/include directory because the preserved install does not export that public header; the include path is explicit and configuration fails closed when it is absent.

Before GlobalConfig::instance() is called, a host-owned preflight parses config.json and requires a JSON object with a JSON-object global section and nonempty relative filenames for config_logging, config_sensors, config_preprocess, and config_odometry; map-enabled sessions additionally require config_sub_mapping and config_global_mapping. Absolute paths, ./.. traversal, lexical or canonical escapes, root/component symlinks, missing or nonregular files, malformed JSON, and non-object component JSON are rejected. The component paths are checked both lexically and canonically, so a symlink cannot redirect a configuration outside the explicit root. This preflight constructs no GLIM object and runs before the process-global configuration lease.

The target uses one C++17 host adapter library and a synthetic fixture test. It is bounded to one preprocessing and one odometry worker. The process-wide GLIM GlobalConfig singleton is protected by a held lease, but the lease is acquired with try_lock: a second same-process configured session fails immediately with kCoreFailure instead of blocking. GLIM configuration replacement and callback slots are process-global; safe concurrent distinct configurations therefore require isolated processes/containers. The slice does not claim coexistence with arbitrary GLIM users in one process.

Runtime mapping and lifecycle

typed host EventStamp/LidarFrame/ImuSample
    -> AdapterBoundary (serialized counters/order/terminal latch)
    -> Phase 3a checked RawPoints/IMU conversion + StampLedger
    -> CloudPreprocessor
    -> public glim::OdometryEstimationCPU insert_imu/insert_frame
    -> staged EstimationFrame conversion
    -> one EOF + one get_remaining_frames drain
    -> atomic trajectory batch seal (map-enabled sessions use the Phase 3c
       trajectory+map batch seal)
    -> complete_drain / take_trajectory

submit_lidar, submit_imu, EOF, drain, output retrieval, and close are serialized by a recursive session mutex. A core call has an active-call guard: a callback that re-enters the session is rejected. The outer call re-checks the terminal latch after the exact core method returns, even if the callback ignored the nested failure; processed therefore cannot be counted after a reentrant fault. Core exceptions, callback exceptions, invalid core outputs, conversion failures, and counter overflow all become permanent terminal failures. close() is idempotent after a successful drain; a synchronous exact-core operation is not hard-interruptible, so bounded hard cancellation remains NOT_READY.

GLIM's public odometry API has no host EOF method. EOF is therefore a host boundary event, followed exactly once by get_remaining_frames(). The current return value of insert_frame() is deliberately ignored: publishing it and then publishing the same frame from a drain remainder can duplicate a trajectory sample. The runtime collects only the marginalized_frames output parameter during processing and the drain remainder vector. The Phase3b fixture starts the NAIVE initializer at nonzero timestamps, submits two initialized LiDAR frames (2 s and 3 s), and verifies that the two drain remainders recover both stamps exactly once. If any bound LiDAR stamp remains un-emitted, drain fails closed with kStampLedgerUnmatched; it never fabricates a trajectory sample.

The StampLedger retains authoritative int64 nanoseconds, counts emitted entries, and requires emitted_count == bound_count at drain. It is separate from global input order and trajectory output order. Phase 3a's lookup/commit two-phase conversion prevents a failed conversion from consuming an entry.

Transactional publication and counters

Core outputs are first staged in a vector bounded by the explicit max_pending_trajectory option; the check does not rely on a container's implementation-defined capacity(). Once the boundary enters Draining, the drain callback validates every staged frame, frame convention, contiguous trajectory order, timestamp monotonicity, and all counter/order capacity before changing any counter. record_trajectory_batch() then seals all outputs as one transaction. A validation or overflow failure leaves no trajectory output counter or order partially updated.

Normal record_trajectory and record_map_chunk success paths increment only submitted/accepted/processed/output/order counters. trajectory_rejected, map_rejected, and failures remain zero for successful outputs; those slots are preflighted only so their error paths can fail closed. If a nested batch already latched the boundary, the outer callback wrapper preserves that first terminal error and does not double-count failures.

Callback and ownership boundary

The exact core exposes process-global callback slots without synchronization or a clear operation. The session installs a weak lifetime fence for the update-frame slot and removes it during teardown; the fence has no output side effect. All synchronous callback exceptions are caught at the session/core boundary and latch failure. This does not make arbitrary retained callbacks or asynchronous callbacks thread-safe: callback-slot mutation, async mapping, and same-process untrusted GLIM callback ownership remain NOT_READY and must use an isolated process until a versioned upstream synchronization API is available.

Phase 2's nine runtime gaps are therefore accounted for by the trajectory slice as follows (the map rows are closed by Phase 3c):

Phase 2 gap Phase 3b status
transactional activation/rollback closed for typed input and staged trajectory seal; no partial output publication
cancellation and bounded shutdown NOT_READY; synchronous core calls have no hard interrupt
callback exception/lifetime handling synchronous exceptions and session re-entry latch; arbitrary global-slot mutation/async callbacks remain NOT_READY
common EOF/drain protocol closed at the host boundary with one EOF, one drain-vector read, and one finalize
idempotent close/unload close is idempotent after drain; lease/resource destruction is ordered, but process-global GLIM coexistence requires isolation
sensor/frame/order ownership closed by the Phase 3a typed contract, ledger, and independent trajectory order
ABI/provenance and install identity exact public installed targets and pinned prefixes are required by CMake; distro/toolchain compatibility remains an external rebuild gate
complete trajectory/map ordering trajectory completeness/order is closed and fail-closed; complete concrete map ordering/coverage/export is closed by Phase 3c
global map save/status status-less save(path) remains unused; concrete synchronous GlobalMapping::export_points() is proven by Phase 3c, while abstract/plugin map modules remain NOT_READY

This trajectory-only Phase 3b evidence does not itself exercise a map object. For the concrete map path and its frame-coverage/point-count proof, use the separate Phase 3c evidence document. Abstract mapping modules and status-less persistence remain fail-closed there.

Synthetic adversarial coverage

The Phase3b executable has the baseline groups listed below; the current combined Phase3b/3c executable adds map success, bounds, coverage, and mapping callback groups as documented by Phase 3c:

  • config preflight rejection for missing keys, malformed JSON, absolute and traversing filenames, and symlink escape, before core construction;
  • exact public-core LiDAR/IMU vertical slice, ordered two-frame input, one EOF, two drain remainders, ledger completeness, trajectory batch seal, idempotent close, and exact nanosecond recovery;
  • callback exception translated to a terminal fault with no publication;
  • reentrant callback whose nested failure is ignored, proving the outer call still fails and imu_processed remains zero;
  • immediate same-process GlobalConfig lease rejection; and
  • counter-overflow fault injection and mapping-contract lifecycle checks.

Phase 0 contract tests additionally prove that successful trajectory/map outputs leave rejected/failure counters at zero, while the Phase 3a tests prove emitted-count/all-emitted ledger accounting and mixed global order. Fixtures are synthetic temporary config/value files only.

Evidence

The superseded final2 and pre-preflight roots remain preserved, but are not authoritative for the current combined target. The historical Phase3b-only normal evidence root is /tmp/glim-clean-room-phase3b-config2-final-normal.big6xu; the immutable historical ASAN/UBSAN root is /tmp/glim-clean-room-phase3b-config2-final-sanitize.oBhbeC. Each root contains one configure/build/CTest/direct/ldd sequence from the final source state; no generated output is part of the source manifest. Normal and ASAN/UBSAN CTest both report 2/2 tests (runtime and source guard), the direct executable reports the historical six trajectory groups, and LDD_UNRESOLVED=0.

root configure.log build.log ctest.log direct.log ldd.log test binary
normal 40afbd479c05b5b822abc61d81de7c19c793ce1258f66169d162e1239cb2be10 85416515a309030168bf734b8ab51a4f5d174922984b02586e247c6b9a63944f 20f0c85cafeb2ec59588046786dd276e60162bfbcede7ac2dba2e2831f25a981 ba66f85e6dce502b033e689dadf3c69ebc2357639eddc02b273a49faac71de4b 8325d505b3d14f62230cacaf7558afd08f00dd0a88cc171c5acdf28295e0eb55 8a58ac756bd331a7fa7e9391770b4cda800a88f71129a4d136de2b75230c9578
ASAN/UBSAN b46d50f2dc87aced9340e8b93b6d45a20d7b6e35f3109b3b10f8bd6a0584fd8e b2db2f9790a205294254454bb010ab50dc2a1014161d533726630fdef7f2edbc 2649049170bb8a5ab5dfc7d61448b90fa1cb37690a2026cf248f5d0d50fae255 27a171233885102ba15b4a7a51b4558eba79c588e971d95520f87416b46b1fa0 94ff7e8c6b18659752e60445a0ec1febdc0f0b645a482f764a85b50e5c1b3de0 051f865dec42b23025414e5c6731bd4ee142bd9f351640332a11c842dd9c2a75

The adapter-tree canonical manifest (generated files excluded) is target_files=31, manifest_bytes=4296, SHA-256 7edeb61286fd4d398172731976332a65b170b51008691c2c4e73e1f90092c6d3. ThreadSanitizer was not run: the pinned core's process-global callback slots have no synchronization, and this vertical slice explicitly requires an isolated process for that unsupported same-process concurrency case.

The post-preflight host regressions are preserved under /tmp/glim-clean-room-phase3b-config2-regressions.NBjD9O. Each Phase 0 and Phase 3a normal/sanitizer CTest run reports 2/2:

run configure.log build.log ctest.log
Phase 0 normal 3a7567d8479a3951776516b0af19931eeabd4d52cc76f02f3cd99f101840baca dbfa4bcbc5f0d981abbde29742454568716f893ea552c0ba7f2a286759edff85 da0b007212ba3d3001511c59e46966ca2251473fe67e21f8ef2ea9a385d225d1
Phase 0 ASAN/UBSAN 7f330d6ab6cc72426fe6ce3b5ef77d583153427c9015ac7906addad4266d7f19 85620a82a7d950aafd484b96d523b5a1da81248c865f6800730c8f408785c4cc 84be7ee8c7a2938845bde575cb708e195b170966a78df6eea58bbeb478fc5a0b
Phase 3a normal 153b3c831724fb45f64c0873208e12d92a0d0ca723eb8cd018173f638afb1488 4b0548bd2dd0209e8a08ff198f798d58cc36223178e1cd267d73bf9a51b4941d b4a823f42d24afb6e38ac0301e90d46fbdcddb0368665011398d9702273b75ae
Phase 3a ASAN/UBSAN 7c6a860732b9f20a017f7c3d0e37616301c4a46465c7df862fd4b6962a5dc52e 48e9cd483425ab26a86a8ba0ed36319ac25a244c8fda879829b0fb4371f38c15 d18a9bf0422550bf4ad7ffbab7e73f5f350b64ae9bb6a02c72df13f55402655f

This phase remains runtime-only evidence. ROS distro integration, live frontend/backend wiring, asynchronous cancellation, abstract/plugin map extraction, same-process callback isolation, benchmark data, and formal claim eligibility remain NOT_READY.