memworld: answer from CLIP embeddings, not Hyperspace - #4101
Draft
jeff-hykin wants to merge 340 commits into
Draft
memworld: answer from CLIP embeddings, not Hyperspace#4101jeff-hykin wants to merge 340 commits into
jeff-hykin wants to merge 340 commits into
Conversation
…world->base_link in tf, corrected scans place by their stamped pose, corrected lidar preferred, ingest stores the corrected tf Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
Server: replay segments cached gzipped in a bounded LRU (the viewer preloads every segment of a long recording), builds mark their last keyframe so a build cut short is rebuilt, the index reads bounds from tags instead of decoding every keyframe, one store lock covers frame reads from the scrubber and the evidence thread, the planner runs over the whole map under a lock, reopening a recording clears every cache, stop() cancels a running build before closing the store, the analysis subprocess no longer imports the whole package for a .db, and the replay frame cache is keyed by stamp (an mcap numbers each window from 0). Hyperspace: ingest writes into a temporary db replaced on success (a rerun no longer doubles every keyframe), tf goes in chronological order (the reader skips ids it has passed), the min_frames relaxation is decided per question, navigate carries and checks the query id, an empty answer clears the pyramids, labels are truncated, the MLS goal snap weighs height, float depth is metres, the SigLIP fallback uses the depth camera's intrinsics. Viewer: disconnect disposes the scene and the mic, scene.dispose frees the GPU buffers, evidence photo textures are released, the UI buttons use handler properties so a reconnect does not stack listeners, an unexpected socket close runs the same teardown, dead canvas removed. module.py is at its size limit again: ClientConn and RevalidatedStaticFiles moved to clients.py. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
… query time
Several checkpoints hallucinate in different places, so the per-cell minimum
(or second lowest) of their contrast scores keeps what they agree on. The
keyframe now carries every member's grid; the vector index keeps the primary
member. NaFlex checkpoints load through the Siglip2 classes with a patch
budget ("id@576"). Query side reads the member specs from the store.
Claude-Session: https://claude.ai/code/session_019gtvzdmySrajw6uGhCy2EZ
Second-lowest pooling over base-patch16-224, base-patch16-256 and base-patch32-256, hot above 0.02, support from one keyframe: cluster precision 0.73 at recall 0.88 on sf_office against 0.37 at 0.88 for the single So400m with the same chain. min_frames is now a module and CLI knob. Claude-Session: https://claude.ai/code/session_019gtvzdmySrajw6uGhCy2EZ
…ble that clears the bar base-patch16-224 + base-patch16-256, per-cell minimum above 0.005, support from one keyframe: cluster precision 0.74 at recall 0.71 on sf_office against 0.37 at 0.88 for the single So400m with the same chain, at 2.7x (Mac) / 5.9x (RTX 5070) the frame rate and 3.0 GB instead of 5.3. The three-member 2-of-3 vote (recall 0.88 at 1.6-3x) stays one config away. Claude-Session: https://claude.ai/code/session_019gtvzdmySrajw6uGhCy2EZ
The round-1 move of depth_info_stream_for left hyperspace_ingest without the import (the repo ignores F821), so every Prepare search died: imported. A stitched .db whose colour stream is a webp CompressedImage now reads as Image (RecordingDb, the codec wrapped at open). tf_static transforms are held for all time and tf transforms are filed under their own stamps. The ingest merges tf chronologically as two streams (heapq.merge) instead of buffering the whole recording. Navigate carries the answer's query id and refuses a stale one; the answer and its id are kept together. Reopening and the world-cache build take the store lock; prepare stops between steps; shutdown leaves the store to the process exit rather than closing it under a live prepare thread. Empty replay streams count as unavailable; raw images honour row stride and endianness; float depth ignores inf; the SigLIP index keeps ids and stamps instead of every image observation; its CLI builds the same tf tree as the server; planned routes may exceed 2000 points. Viewer: the replay controller is disposed on disconnect (timer, downloads, bitmaps), evidence and thumbnail bitmaps are closed with their textures, eviction skips the segment on screen instead of stopping. DEMO.md describes the refine switch correctly. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The round-2 store lock self-deadlocked: the world cache build re-enters it through the replay keyframe read, so it is an RLock, and every path now takes the locks in one order (world cache, store, replay, index); the segment route resolves and reads the replay under one lock, _ensure_store is locked, and the SigLIP index loads under the store lock. The mcap store's count() is 0, so the "last keyframe" marker never landed and every start rebuilt the replay (30 min on grocery): the build now uses one scan of lookahead. tf_static: detected only by name, its edges are static (one sample held for all time, unbounded span), and the ingest writes them once before the window. tf_static-less recordings no longer refile every tf transform at t=0. The corrected-odometry edge uses the header stamp like every other edge; the replay CLI builds the server's tf tree; the SigLIP frame table keeps pose_tuple; answers are serialized through publication; the text-embedding cache is bounded; stop() closes Hyperspace and the SigLIP index only once prepare is done; the package __init__ imports its module lazily so subprocesses that need recording.py start in 0.3 s instead of 10. Viewer: the frame cache never closes the frame on screen, a frame decoded after disconnect is dropped, the minimap texture is disposed when replaced, dead Flight.pose and _topDownTex removed. Tests: empty keyframe stream, padded big-endian depth rows, static edge span. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
Locks: the order is now world cache, replay, store, index on every path; the replay build runs under the replay lock alone (its streams have their own connections) so scrub and index requests are not held for the 30-minute build; the world cache takes the store lock only around the image and trail reads; the segment route reads a replay it resolved outside the lock and answers 503 if the recording was reopened meanwhile; _camera_hfov and the SigLIP index init are guarded; the SigLIP index build and the fallback's depth reads take the store lock; the initial payload queues the answer snapshot under the clients lock; the evidence thread resolves the image stream inside the lock. Naming: the lidar is picked against the resolved world frame (the tf root), not the configured one, so the tf-agreement check works. The ingest stamps corrected poses with the header time like build_tf_tree. A once-published tf edge spans to infinity as at() holds it; segment diffs are keyed by scan index (two scans can share a stamp); the text-embedding cache is bounded. Viewer: eviction keeps the segment being sought, a frame that finished loading after the scrubber moved on is cached but not shown, the replay ramp uses all four colour stops. Replay serving (index json, orbit positions, camera frame) and the colour stops moved to replay_serving.py: module.py was over its size cap. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The SigLIP index init took the index lock before the store lock (store first, like everything else); the fallback search counts and searches the index under the store lock. Keyframes are found by scan index, not stamp: two scans can share a stamp, and then the final map, a segment's keyframe and the "last" marker all picked the earlier one (test added). Viewer: eviction runs after a seek is applied so the segment just left is dropped, and the frame cache is an LRU (bumped on a hit). A tf edge published once spans onward (test added). Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
Store reads that ran outside the lock now take it: the tf tree load, the depth camera intrinsics, the costmap, and the camera frame (memoised, since the replay build asked per scan). Refine is also skipped when the heat's bounding box exceeds 50 M cells, not only when one axis exceeds 60 m. The ingest substitutes the corrected odometry only where build_tf_tree does (a direct world->base_link edge). Embedding-job progress reads the pipe as bytes arrive instead of blocking for 256 characters; its process handle is cleared under the lock. Reopening resets the tf tree before renaming streams; the replay index json is read under the replay lock and a reopen in between answers 503; dead branches removed (an unreachable stamp fallback, an unused MLS snap, a NaN floor fallback); the MLS docstrings say what the code does; the torch-threads log prints the real "before"; the ReplayServing contract lists _camera_pose_of. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The SigLIP fallback resolves its index inside the store lock where it counts and searches (a reopen between the two swapped the store under it), the index status does the same, and adoption stops the old index under store then index. A tf edge publishes its sorted stamps last, so a lookup racing the lazy init never sees the stamps without the positions. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
A failed replay build is remembered instead of rebuilt on every request; the replay routes answer 503 while a build holds the lock; the ingest substitutes corrected odometry only where build_tf_tree does; the planner lock joins the lock order and guards the orbit cache; the camera frame is memoised even when empty; a Hyperspace reload mid-question fails cleanly; the last unlocked store reads are locked; diff windows use min/max stamps. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The visual index is dropped with the store it read, under the locks; the index warmup stays under them; the replay and its index json come from one non-blocking acquisition; the Hyperspace ingest follows build_tf_tree's own corrected-odometry decision (never an empty stream), with a test. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The index fetch surfaces the server's detail; the viewer polls while the replay is building and stops with a status line when the build failed, and a failed segment fetch says so on the timeline. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The SigLIP fallback (visual index, embedding job, depth-placed answers) moves out of module.py into visual_answers.py. The replay build keeps one failure string the viewer can read, remembers failures after the build step too, and treats a cancelled build as failed but retryable; /orbit and /navigate answer 503 during a build; HyperspaceSearch releases what it opened when construction fails; voxel support counts every yaw bin like Hyperspace's refine expects; the structural gate keeps frames with no covered cells; caches a reopen clears are read once; the camera frame memo lives beside the other derived caches; dead scene array, unreachable guards, a 3x undercount in the MLS log line, an inert A* argument and four stale comments are gone; the replay index warns on non-ascending stamps. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The initial payload sends one snapshot of the world caches taken under their lock; SigLIP evidence for a superseded question is not published; the pool test pins the yaw-bin support. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The desktop scene's window and document handlers are aborted on dispose, so a reconnect does not stack them; /orbit and /navigate answer 503 with the replay's own progress string for any replay failure. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The depth stream is resolved under the same lock as its read; the replay availability check sits inside the failure bookkeeping; a reopen resets that bookkeeping before the slow open; a replay with fewer than two scans is refused rather than polled forever; a dead guard in the structural gate is gone. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
A locally built visual index is named per image stream and model, and tagged and checked for its stream; an embedding job clears only its own process handle; yaw-bin support counts every hit, like Hyperspace's; a spoken query is capped at the result's length; the replay routes never build on a request thread; the static map falls back to accumulated scans when the replay is unusable; the viewer pauses autoplay on a failed segment and surfaces the server's reason; tests for the lidar-name check and the bin support. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
An empty final keyframe falls back to accumulated scans; the build a route starts is tracked, started once, never while stopping, and joined at stop; the Hyperspace ingest takes the module's stream choice; a viewer connecting while the map builds is told so; the loading label re-reads its target; yaw-bin support uses a dense mask; the SigLIP answer names its engine and query; the local index name needs its image stream; tests for the 400-char cap and a foreign camera's index; comments. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The camera_info pairs with a configured colour stream in the module and the ingest alike; the ingest checks named streams exist and stops what it opened on a failed setup; the replay worker handle is published after start and the worker re-checks stopping; the viewer's disposed replay leaves its loading-label timer inert; the ingest usage lists the stream flags and ingest_command has a test. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
Query results are queued to viewers under the clients lock; the camera_info pairing also accepts the <prefix>_camera_info form; the replay CLI refuses world-aligned scans before touching streams; an empty camera_info stream leaves the default field of view; prepare names the streams before forwarding them; stop() snapshots worker handles under the replay lock; the viewer drops every controller on disconnect and their in-flight replies do nothing; tests for the pairing and a refused stream. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
A brief workers lock publishes the route-started replay worker so stop() never waits on a build; scans stored in the configured world frame are world-aligned; the embedding job opens the store first; a replay needs an image stream and says so before deleting anything; an uncalibrated camera_info counts as none; a SigLIP answer clears the Hyperspace overlays; the viewer's disconnect clears its status polls and pending buffers and ignores frames of a closed socket; tests for both camera_info pairing forms and the CLI's refusal; docstrings. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
stop() marks stopping and snapshots the workers under the workers lock, paired with the worker start; only the configured world frame or a stitched frame holds world-aligned scans, in the module and the replay CLI alike; Hyperspace overlays update only while their answer is current and go out under the clients lock; the camera frame's payload is read under the store lock; a clearer message without an image stream; the viewer's disconnect resets its button labels and timeline classes, drops a late answer, ignores a stale socket's error, never re-arms a poll, and a fresh scene takes the layer checkboxes; config comments. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
A reconnect's overlays are queued under the clients lock; replay streams carry the world frame they were built in; both CLIs resolve a world frame tf does not know to the tf root, and the replay CLI refuses to leave an empty replay behind; a fixed-frame lidar in a recording without tf is still world-aligned; the viewer's ask() answers only its own session, the cloud's checkbox state and the replay's hiding are kept apart, and the minimap box keeps its button label honest. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The local visual index is tagged with and checked against its world frame; the replay CLI deletes the empty streams of a build that placed nothing and says why; a fixed-frame lidar tf cannot place is the world; --world-frame help in both CLIs; the viewer re-enables the ask button on disconnect, the tour reads the cloud's box flag, and keyboard toggles write the layer boxes and the map button back; tests for a replay built in one frame, the CLI's tf-root fallback and its empty-build exit, and a foreign-frame index. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
The visual index CLI tags its build with the frame it resolved; the module rejects a replay build that placed nothing, deletes its streams and remembers the failure; the CLI test checks an empty build leaves no streams behind. Claude-Session: https://claude.ai/code/session_01JJfsvX2QuFdjjWLynXsdwz
`90b84a2c0` fixed the tunnel -- one SLAM jump interpolated into a straight line that erased the wall it crossed -- by erasing only at the pose SAMPLES. That traded one bug for its opposite. Whenever the samples are further apart than the robot is wide, the robot's own body and the people walking beside it sit BETWEEN the samples and are then read as walls: a straight 10 m corridor sampled every metre, with one such voxel at each midpoint, planned no route at all where a 9.20 m one runs down the middle of it. Neither extreme is right and no constant separates them either: the two cases that have to be told apart are a 1.0 m leg on a downsampled recording (a drive, and its body voxels must be erased) and a 2.0 m leg on a densely sampled one (a jump, and bridging it erases a wall). Any fixed threshold between them is wrong for one recording or the other. `bridgeable_gap` takes it from the recording's own sampling instead -- twice the median leg, capped at MAX_BRIDGE_M -- and `densify` grew a `max_gap` that leaves a longer leg alone. Both cases now hold, and each is a test that the other's extreme fails: bridging everything reopens the wall, bridging nothing walls off the corridor. Found by round 77, which reproduced the corridor on a fixture I had not thought to build. The corridor and the floor height still come from the unconditional dense line, which is what they are for.
…itten `plan` snaps the goal `within=reachable_from(start)`, and nothing exercised it. The point of that mask is not that an unreachable goal is refused -- A* would refuse it anyway, which is why every fixture I tried first had no power over the line. It is that the goal is snapped to the nearest cell the start can REACH rather than the nearest cell of any kind, so a thing standing against a wall is approached from the side the viewer is on instead of being declared unroutable. So the goal sits just past a solid wall between two rooms the robot drove separately: its nearest free cell is 0.2 m away on the far side, the nearest reachable one 0.75 m away on the near side, and both are well inside SNAP_RADIUS_M. Removing `within=reachable` turns a route into no route. My first attempt put the goal deep in the far room, where A* said no on its own and removing the mask changed nothing: a test that passes against the broken code, which is the defect class this whole review has been chasing. Recorded because the fixture is the whole difficulty here.
**The worst kind of bug: the recording with the most work already done in it was the one that could not answer a question, and the DEMO walks the reader into it.** `find_in_memory` has used the embedding index alone since "one engine answers". Two places had not been told. `_prepare` still SKIPPED `_build_visual_index()` whenever Hyperspace had loaded, and `_index_status` still or-ed in `_hyperspace_ready()`. So on a recording carrying a Hyperspace index the viewer was told search was ready, Ask and the microphone were enabled, the "Add embeddings" button was hidden because there was seemingly nothing to add -- and every question came back INDEX_NOT_READY, "the SigLIP index holds no frames", with no way out of it offered anywhere in the UI. The embedding index is now built whatever Hyperspace holds, `present` means what it says, and the client's "can ask" reads the embedding index alone. The status line says "Hyperspace index: N keyframes (not used for questions)" instead of "Search: Hyperspace", which is the sentence that made this take a round to find. **`fold_static_tf` corrupted the tf and deleted the evidence.** `TfTree` reads `/base -> /cam` and `base -> cam` as one edge; this keyed them raw and read them as two. So the stale MOVING copy survived the filter, outvoted the folded static -- it is a later sample of what the tree sees as the same edge -- and `tf_static`, the only record of the right value, was deleted. `odom -> cam` at t=2 read 4.0 before the fold and 6.0 after, unrecoverably. Reached from the ingest and from the calibrator, both of which write the user's recording. DEMO.md documented the broken flow, and four comments still said an mcap "cannot be written to". The format is editable and appendable; it is `McapStore` that reads only.
**The ingest deleted a good index and then refused.** The guard block exists precisely so nothing refuses after the deletes, and its own comment says the mistake has been made twice before. I made it a third time last round: the colour/depth alignment check I added to `_ingest` runs after them, so a `--depth` naming a stream on another clock destroys the index that was already there. The probe moved into the guard, using the same `IngestConfig` the ingest will. **`_spawnAtCentroid` was the last open-coded robot-to-three transform**, and it was missing the world spin AND the scale. Against three's own matrixWorld for a 12x10 m map, for a call whose whole job is "1.50 m ahead": 1.50 m of error at rest, 4.47 at scale 0.5, 5.64 at a 30 degree spin, 12.62 at 90, 16.38 at 180. It runs at the end of "Building the map...", which is minutes on a first run -- exactly when someone pinches to zoom or turns with the stick. **`tour.js` held the last unspun `atan2` yaw.** Same measurement as the `results.js` pair: the error is exactly the world's turn angle. **`touch.js` counted every finger on the SCREEN, not on the canvas.** The walk stick is a sibling element, so a finger resting on it made a one-finger look drag count as two: look went dead, a phantom stick was written over the real one, and the world silently rescaled on a "pinch" that was one finger and a joystick. `main.js` already used `targetTouches` with a comment about exactly this. **A controller that vanished never sent a stop.** "Always emit a locomote (zero stick = stop)" was gated on having SEEN the controller, so one put down, or lost to hand tracking, left the last stick value integrating for ever: 450 degrees of world yaw and 7.50 m walked over five seconds with no controller present. **The answer panel's ellipsis emptied the line it trimmed.** It shed whole words, and `breakLong` guarantees a long token's line has none: 400 'z's drew a fourth line that was literally " ...", with 89 characters of room going spare. Shedding characters shows 368 of 400 instead of 279. **`max_places` had no upper bound** while `MemoryQueryResult.clusters` stops at 64 -- the same pairing, and the same bug, as `object_radius_m` two rounds ago. One `MAX_ANSWER_PLACES` now. **A folded static lost its pre-motion coverage on a zero timebase.** `TfTree.from_stream` reads `transform.ts or obs.ts`, so a transform stamped exactly 0.0 is read as UNSTAMPED -- and 0.0 is what a fold restates a static to hold from on a recording whose clock starts at zero. The row carrying the folded statics is now stamped at that moment too, so both readings agree.
…o of everything **`fold_static_tf` retimed the moving poses it was supposed to leave alone** -- a regression from the zero-timebase fix one commit ago. That fix put the folded statics into the first existing row and moved the row's stamp back to `first`; `TfTree.from_stream` reads `transform.ts or obs.ts`, so every MOVING transform in that row without a stamp of its own moved with it. Measured on a two-row stream: `world -> base` read 0 m at t=2 before the fold and 1 m after, with a pose appearing at t=1 where there had been none. One fix, one new corruption, in the same function, found by the next round. The statics now go in their OWN row stamped at `first`, so nothing else moves. Both properties are tested, and each test fails against the other's version. **A vanished controller kept holding what it had been holding.** Last round taught the adapter to send a stop when a controller disappears, and stopped at locomotion and yaw: - Its pinch state survived, so `_updateBimanualScale` went on scaling from a hand that no longer exists -- both gripping, right disappears, left moves, and out comes `scale_delta` with a factor of 2. - Push-to-talk is the right controller's A button, and losing it mid-utterance left `voice_start` unmatched. `main.js` ends a recording on `voice_stop` and on nothing else, so the microphone stayed on with nothing able to turn it off. Both cases driven through the real `InputAdapter` in node: 1 phantom `scale_delta` and a lone `voice_start` before, 0 and `voice_start, voice_stop` after.
**`memworld --stop` left siglipify writing to the recording.** The real command is `nix run <flake> -- run <recording> ...`, and `nix run` commonly FORKS into the built program rather than exec'ing it -- so the work happens in a grandchild and `process.terminate()` never reached it. `stop()` calls `terminate()` and does not join the thread, so the module reported itself stopped while the embedding subprocess was still writing into the db the next server would open. The job also went on reporting "running", because the read loop blocks on a pipe the grandchild still holds. Measured: a stop at 0.5 s had no effect until the grandchild finished on its own six seconds later. Own session, signal the group, and SIGKILL what ignores the TERM. **One pause in the drive walled off the corridor.** `bridgeable_gap` takes the median leg and filtered out only legs of exactly zero -- but real odometry never reports the same pose twice, so five seconds of standing still fills the list with millimetre legs and drags the median to nothing. Then nothing is bridged and the robot's own body reads as walls: same voxels, same drive, same endpoints, and `max_gap` went from 1.5 m to 0.004 m with the 9.20 m route to none at all. The only case the old filter caught was the perfectly still one, which is synthetic. **A refusal during the index build wedged the viewer silently.** Both handlers were bare `except Exception`, and a refusal here is a SystemExit. `index.build()` reaches `refuse_if_a_rebuild_is_half_done`, so a recording left with `tf` and `tf__rebuilt` raised straight through -- and `threading.excepthook` ignores a SystemExit out of a thread without printing anything, so the prepare thread died in silence, `_index_progress` stayed on "building (had 0 frames)", and the viewer polled that every three seconds for the rest of the session with no reason shown anywhere. `_ensure_world_cache`, `_build_replay` and `_load_hyperspace` all already catch `(Exception, SystemExit)`, two with comments saying exactly this; the function beside them, doing the same job for the other index, did not. Making it run on every recording widened the exposure. **The answer panel's character shedding split surrogate pairs**, leaving a lone high surrogate drawn as tofu. `breakLong` a few lines up iterates by code point for that reason; the trim now does too.
**The pause fix reopened the tunnel.** #41 filtered out legs at or below one cell before taking the median -- and on a drive sampled FINER than the cell, which a 5 cm odometry on a 10 cm grid is, that removes every driving leg and leaves only the jump. The jump then IS the median, gets bridged, and the wall it crosses drops from cost 100 to 90 with a 7.90 m route straight through it: exactly the defect #7 fixed, reintroduced by the fix for #41. A city-scale map (1 m cells, 0.5 m odometry) is the same shape. So the statistic was chosen by measuring instead of by argument. The three candidates against all four known recording shapes: median, all legs fails the pause (bridges nothing) median, legs over a cell fails fine sampling (bridges the jump) 90th percentile passes all four The 90th percentile needs no filter at all: it already sits above the small tail a pause makes and below the large one a relocalisation makes. All three shapes are now tests, and neither rejected statistic survives them. **`terminate()` could not escalate, and could not reach a surviving worker.** The SIGKILL sat in `_run`'s finally, which the thread never reaches because it is blocked on a pipe every member of the group holds open -- a child with SIGTERM set to SIG_IGN was still alive twelve seconds after a terminate() meant to end it. And `_signal_group` returned early when the launcher had exited, which is precisely the case where the worker is still running: `nix run` exits 0 and leaves it. The group id is taken once at spawn and stays valid while any member lives, and `terminate()` now does the TERM, the wait and the KILL itself. **Three more things a controller does not let go of.** Round 78 cleared locomotion and yaw; round 79 found the rest: - the voice guard tested `seen.right`, which counts ANY input source of that handedness -- including the tracked hand that replaces a controller the moment it is put down, which is one of the three ways its own sibling comment says a controller goes away. So the microphone stayed on. - `_teleportArmed` was never cleared, so the arc stayed drawn through the outage and the frame the controller returned the viewer was teleported to a target aimed before it. `main.js` has had a `teleport_cancel` case since it was written and nothing had ever emitted one. - a controller still listed but no longer TRACKED kept its last pinch and position, so bimanual scaling went on from a guess: `scale_delta` factor 2. All six disappearance cases are driven through the real adapter in node; four of them failed before this.
**A finished job killed its successor.** My own group-signalling from round 79 read `self._pgid` in the cleanup -- and that field belongs to whichever run is CURRENT, which by then can be a later one. B started in the window after A published "done", A's finally signalled B's group, and B died with -15 without ever running the adoption its embeddings needed. `process` was already held as a local for exactly this reason; `pgid` is now too. The same ownership mistake the `_run_id` guard was added for, one field along. **A stop landing in the spawn window sent a bare TERM** to the immediate child and nothing else, because `_pgid` was not set yet -- no group, no escalation. It goes through the same `_stop_group` now. **`bridgeable_gap`, third time, and this time the premise was checked.** Its docstring claimed "real odometry never repeats a pose exactly". That is false for the only production caller: `replay._held_through_gaps` REPEATS the previous pose through a tf gap, so a tf stream that stops partway leaves a tail of zero-length legs -- and a tour that parks at three places for 45 s each is 93% stationary with no defect at all. A 90th percentile falls into that noise the moment the stationary samples pass 90%, and the corridor walls off again. No single statistic does this. It takes two thresholds doing two different jobs: an absolute one (a centimetre) to say "this leg is not driving at all", and the median of what remains to say "this leg is not like the driving". An absolute number cannot do the second -- 1.2 m is a jump on a 5 cm drive and a step on a 1 m one -- and a quantile cannot do the first. All four statistics tried across three rounds are now each killed by one of six tests: coarse drive, coarse drive with a pause, fine drive with a 1.2 m jump, fine drive with a 2.8 m jump, a tf gap that makes 91% of legs exact repeats, and a tour parked for 93% of its samples. **Three more things a controller does not let go of**, all siblings of round 79's: hand-tracking joints going untracked kept the pinch (`scale_delta` factor 2 off a hand with no pose); the ray pose going away while aiming still committed the stale target on release, because the controller is still listed so the vanish guard never fires; and the pose-loss branch cleared `pinching` but not `wasPinching`, which is the only thing that picks the 40 mm release threshold over the 25 mm press one -- so two hands held 32 mm open, a gap the adapter itself calls not a pinch, scaled the world by 2. **One non-finite point made a recording permanently unviewable.** The height filter tested only Z, so a NaN in X or Y sailed through, the cloud header's min/max went NaN -- sent to every viewer as the world's bounds -- and `np.histogram2d` raised after `_cached_cloud` was already assigned, so every later build failed identically. `global_map` is a raw PointCloud2 from somebody else's pipeline, which is where such points come from. Everything else in that fall-through degrades and tries the next source; this was the one place that did not.
**`bridgeable_gap`, fourth time, and this one has no threshold to be defeated.** Round 80's "median of legs over a centimetre" dies on a drive sampled every 5 mm -- the filter removes every driving leg and the JUMP becomes the median, wall cost 100 to 90, a 7.90 m route through it. Which is exactly how the round before died on a drive sampled every 5 cm, one scale up. Every one of the four statistics tried was COUNT-weighted with a threshold bolted on, and every threshold ate a drive sampled finer than itself. It is now the leg length at which the cumulative DISTANCE reaches half the total. Distance is the right weight because the question is how far the robot moves between samples, and it is immune to both tails without a threshold anywhere: standing still contributes no distance however many samples it takes, and a relocalisation contributes distance but only once. Seven shapes are tests, and all four rejected statistics are each killed by one of them. **A non-unit quaternion made a matrix that is not a rotation.** `pose_matrix` built it from the raw components, so a 90 degree turn published with every component twice too large -- a unit mix-up, a corrupted field -- gave det(R) = 25 and sent (1, 0, 0) to (-3, 4, 0) where the answer is (0, 1, 0). Wrong, not imprecise, and silent: that matrix places the camera, the lidar and the global map, so one bad tf sample moved the whole world. All zeros, which is what an uninitialised message looks like, is now the identity rather than a matrix that collapses every point onto the origin. **Losing the ray ON the release frame still committed.** The guard added last round only runs while the trigger is HELD, so a release that coincides with tracking loss took the commit branch and teleported to the target aimed before it. A release is only a commit if we can still see where it is aimed.
**Putting a gripped controller down scaled the world.** The controller branch wrote `wasPinching` from the GRIP BUTTON, and `wasPinching` is the joint pinch's hysteresis -- the only thing that picks the 40 mm release threshold over the 25 mm press one. When a controller is put down the input source stays listed and its pose is never lost, so neither the vanish loop nor the pose-loss branch clears anything, and the first hand-tracked frame judged a hand that had not pinched by the looser rule. Two hands held 32 mm open -- wider than the press threshold, so not a pinch from cold -- came out as a pinch and scaled the world by 3. The grip no longer writes a flag that belongs to the joints. **`slerp` divided by a zero norm**, so interpolating toward an all-zero quaternion -- what an uninitialised `geometry_msgs/Quaternion` serialises as -- made every stamp between two samples NaN. Those NaNs reach the orbit trail on the wire and the route planner's own path, where `from_voxels` raises "cannot convert float NaN to integer", and `_orbit_cache` and `_route_planner` hold the failure for the life of the module. Stated accurately: the `pose_matrix` normalisation in this same round already closes that path on its own, because a NaN quaternion's norm is not greater than zero and it hands back the identity. The `slerp` guard is the second of the two, and the test asserts the PROPERTY rather than either implementation -- removing both is what fails it, which is what a mutation run showed and is why the first version of that comment, which claimed pose_matrix did not cover it, was wrong.
…f the whole
Five global statistics were tried over four rounds and every one was defeated by
a real recording shape the one before it had not met:
median of all legs a pause; millimetre legs become most of the count
90th percentile standing still passing 90% of the count
median of legs over 1 cm a drive sampled every 5 mm: the filter removes every
driving leg and the JUMP becomes the median
median of legs over a cell the same, one scale up, at 5 cm
distance-weighted median fifteen minutes parked at 10 Hz -- 9000 jitter legs
of a millimetre accumulate 9 m, outweighing a 10 m drive
The pattern was not four unlucky choices. A recording is not homogeneous: it has
stretches of driving at different speeds, stretches of standing still, and the
occasional relocalisation, and no single number describes all of them at once.
A jump, though, is always unlike the legs immediately AROUND it, whatever the
rest of the recording is doing. So the test is now per leg and local: a rolling
median over a window of legs, and a leg is a drive if it is no more than twice
that -- floored at one cell, because below that bridging adds no cells at all and
cannot matter, and capped at MAX_BRIDGE_M. `densify` takes the resulting mask
rather than a single number.
All eight shapes are tests, and each of the five global rules, plus "bridge
everything", is killed by at least one of them.
**`rigidly_joined` measured raw quaternions.** The dot product it uses is only a
cosine for unit ones, so two samples of the same identity rotation published as
(0, 0, 0, 2) gave 4.0 where 1.0 means "has not turned" -- and a rig that IS
rigidly joined was reported as not joined, refusing the calibration before it
measured anything. Every other reader of an orientation in this package
normalises now; this was the last one that did not.
All three are in code no recent round had touched, found by asking what recording SHAPE breaks it rather than by reading the diff. **A frame published onto itself hid the tf root.** `tf_root` is `parents - children`, and a self-edge puts its frame in BOTH sets -- so one such edge, which a duplicate or misconfigured broadcaster really does produce, subtracts the true root out and returns None for a tree that has exactly one. `name_streams` then left `world_frame` at its default, the tree did not have that frame, and every lookup afterwards returned None: a recording whose tf is otherwise perfectly usable placed nothing at all. **A voxel index past the packed range wrapped silently.** Each axis gets a fixed 21-bit field, and a UTM-referenced map at 0.1 m voxels sent (5000000, 4000000, 10) to (805698, -194304, 10) -- wrong, sign-flipped, and with nothing said, so every answer came back placed somewhere else entirely. `replay.pack_keys` has refused exactly this since it was written; the one in `hyperspace_fast`, doing the same job for the other index, counted on never being asked. **An analysis answer drew a route to where the viewer was already standing.** `_navigate_to` refuses a route whose two ends are the same point, with a comment recording it measured live -- three copies of one pose returned as a successful 200. `_add_route_to_result` calls the same planner, runs automatically on every analysis answer carrying a focus point, and only counted the points.
**A jump in the first or last few legs was bridged.** The rolling window has to be padded at the ends, and scipy's `nearest` pads with the edge leg ITSELF -- so a jump there made up most of its own neighbourhood, looked ordinary, and went through. Six legs is enough: `[1.2, .05, .05, .05, .05, .05]` bridged the jump, took the wall it crosses from cost 100 to 90 and planned a 4.5 m route through it. `mirror` pads with the legs on the other side without repeating the edge one, so the first leg is judged against the legs after it -- and that alone broke the pause fixture, which only the full route suite caught. A leg on the BOUNDARY between a drive and a pause has a neighbourhood that is half each, and the median there picks whichever half is bigger. The question is whether a leg is comparable to the LARGEST ordinary legs nearby, which is a high quantile. Both choices were then made by measuring against all twelve known shapes rather than argued: with mirror padding, everything from about the 50th to just under the 90th percentile passes all twelve; the 90th lets an end jump through and the median fails the pause. The 75th is the middle of what works, and `nearest`, the median and the 95th are each killed by a test. **A voxel on the edge of the packed range could no longer be probed.** A neighbour probe walks one voxel out, so an edge voxel generates a neighbour past the range -- and making `pack_keys` refuse an out-of-range index, which it must or a UTM-scale map wraps silently, took a VALID boundary voxel down with it: `near_scene` and `cluster_voxels` raised where they had worked. Every voxel that can be in the scene is in range, so a probe that is not cannot match anything; `pack_probe_keys` gives it -1, which no real packed key can equal. Live on sf_office1_2 after the change: routes of 35.75 m and 28.62 m from two points on the driven path, both to a photograph rather than the cluster centre -- unchanged from before it.
…line
`navigate()`'s success path checks `_disposed` after its await; its failure path
did not. A `/navigate` still in flight when the viewer disconnects keeps running,
and its rejection wrote "Route failed: ..." into the status line -- the same DOM
node `setStatus('Disconnected')` uses, and the one every LATER session uses too.
`replay.js`'s `_pumpFrame` writes the rule down in a comment -- "the success path
above checks this after its await; so must the failure path" -- and applies it in
both branches. This was the one place it had not been.
Driven against the real class with a fetch that rejects after `dispose()`: the
status line stays "Disconnected" now and read "Route failed: network drop"
before, while a live session still reports the failure as it should.
…at all
**A real drive outnumbered in its own window was refused entirely.** Two ways a
recording does that without the robot slowing down:
`replay._held_through_gaps` repeats the previous pose whenever tf has no sample
within tolerance, so a 1 Hz tf chain against 10 Hz scans is NINE exact repeats
per real step. Counting them, every real leg lost its own window to stillness
and the corridor the robot drove down planned no route at all.
A tour that parks EITHER SIDE of a drive rather than only at the end. Half a
second of stillness each side is enough; the shipped pause fixture only parks at
the end, where the padding happened to save the last leg by 11 votes to 10.
So the comparison is now against the other MOVING legs near a leg. Three separate
things, each of which has been a defect of this function:
per leg, not a statistic of the whole path -- five global statistics, five
recording shapes that killed them;
the OTHER legs, never itself -- a leg that votes on its own neighbourhood can
always justify itself, which at the ends of the path is exactly what happened;
only the MOVING ones -- standing still says nothing about how far the robot
travels between samples.
`STILL_M` now decides only which legs INFORM the comparison, never which are
bridged, which is what makes it safe where filtering AT it was not. Fifteen
recording shapes are tests, and both halves of the rule are separately killed.
**A RealSense with aligned depth had no depth stream.** The depth role
disqualified anything containing "color" -- but `pick("depth", depth_like=True)`
has already restricted the candidates to depth-named streams, so that word could
only subtract a legitimate one. `align_depth.enable:=true` names its stream
`camera_aligned_depth_to_color_image_raw`, and the ingest refused "has no depth
stream" on a recording that plainly has one.
**A folded static kept `tf_static`'s spelling.** `TfTree` canonicalises; a reader
outside this package need not, and dimos' own `MultiTBuffer` keys the raw pair --
so a recording spelling the edge both ways came out of the fold holding
`odom -> base` and `/base -> /cam` together, with no chain through it for such a
reader at all. The folded edge is written in the moving stream's spelling now,
and in its CONVENTION for a frame the moving stream has never mentioned.
`test_recording.py` crossed the 75 KB hook, so the thirteen `fold_static_tf`
tests are `test_fold_static_tf.py`. Same 312 tests before and after.
Live on sf_office1_2 after all of it: routes of 35.75 m and 28.62 m, unchanged.
`rebuild_stream` drops the original and then writes it back, so a rebuild dying INSIDE that window leaves the name present and holding nothing. The half-done guard asked only whether the name existed, so it let that through -- and the empty original is then dropped from the ranking as having no payload type, leaving the staged copy the only candidate of its type. The module read a half-written rebuild as the recording's own tf, which is the one thing that guard exists to prevent. The comment beside it said a staged copy whose original is present "needs no special case -- it is the longer name of the two and the ranking never prefers it". True only while the original is a candidate at all. A staged copy is now never a candidate for the recording's own stream, whatever it holds, and an empty original counts as not there.
…tarted **Analysis could permanently alter the operator's recording.** `open_recording` hands back a read-write store -- there is no read-only mode anywhere -- and the bootstrap passed it straight to caller-supplied code. A snippet that appended a stream left it in the recording for good and `analyze_memory` reported success: measured, an injected stream survived and the `.db` grew by 24 KB. The store the analysis sees now spells out the READ surface rather than blacklisting the writes, because a blacklist misses the one that matters -- the first attempt here wrapped only objects that already had an `append`, so `store.stream(name, type)` handed back the real thing and wrote anyway. It stops the mistake, which is the whole of the risk: the code is whoever asked the question's own, and one determined to write could import sqlite3 itself. **A timed-out analysis left behind whatever it had started.** `subprocess.run(timeout=...)` signals only the child it launched, and analysis code is free to spawn: a snippet that started a background loop and then slept past the timeout was reported as EXECUTION_TIMEOUT while that loop went on writing, unmanaged, with nothing left holding a handle to it. Its own session, and the timeout ends the group -- the same lesson `embed.py` learnt about `nix run`.
**Padding invents a leg out of the one being judged.** "nearest" repeated the edge leg, which round 83 fixed by reflecting instead -- and "mirror" reflects a leg back into its own window two places along, which is just as good a vote. Parked, one 1.2 m relocalisation, parked: the jump was the only moving leg in the recording, its own reflection was the only thing near it that moved, so it vouched for itself and opened a 1.2 m door in the wall it crossed. Nothing is padded with data now: an end leg sees only what is really beside it. **A real step with nothing moving near it was refused.** `_held_through_gaps` repeats the pose through a tf gap, and at eleven repeats per pose -- a 1 Hz tf chain against an 11 Hz scan stream -- the steps either side of a real one fall OUTSIDE the 21-leg window. Every step of the drive was then alone in a window of pure stillness, took the one-cell answer, and the corridor planned no route. One and ten repeats worked, so the window's width was the whole of the difference. A leg no moving leg is near is judged against the rest of the path's driving instead, which is the only other evidence there is. "Other" is the window's rule again -- one copy of the leg itself is removed -- so a lone displacement that is the only thing moving in the whole recording still has nothing vouching for it. That is what keeps the two shapes above from needing opposite answers. Seventeen shapes now. Both new ones fail on the previous implementation.
… one channel **A transform on its way out named the edge that stays.** The fold learns how the moving stream spells each frame so the folded static chains with what is left -- and it learnt it from every transform in that stream, including the stale copies it was about to delete. The stale copy is USUALLY the one spelled the other way: that is why the recording has two spellings at all. Measured with dimos' own `MultiTBuffer`, which is what reads these recordings: a tf carrying `/base -> /cam` beside `odom -> base`, with `base -> cam` on tf_static, answered `odom -> cam` 4.0 before the fold and None after, the static having been written `/base -> /cam` to chain with an edge that was no longer in the stream. Round 83 fixed the spelling and left the source of it open. **Named like depth is not the same as being depth.** A `colorizer` node publishes depth as RGB for a person to look at, under a name like `depth_color` -- shorter than `camera_aligned_depth_to_color_image_raw`, and length is how the ranking breaks a tie. The RGB one won, and `patch_world_position` raises "too many values to unpack" on it, because a metre is stored in one channel. The channel count BREAKS THE TIE rather than disqualifying. A stream says how many channels it has only as clearly as its codec lets it: a db whose images went through the default jpeg codec hands back three channels of RGB whatever went in, and a recording whose only depth is stored that way should still be found. Real recordings store depth losslessly -- the grocery recording's `depth_image` is `lz4+lcm` and reads back (720, 1280) uint16 DEPTH16.
A run of ten 0.8 m legs inside 0.3 m driving can supply at most nine of the twenty other legs in a 21-leg window, so it could never move the median off its slower surroundings: every leg of the run was refused, the corridor was cut at each one, and `plan` returned None on 31 m of straight, driven floor. Eleven in the run -- one more leg, the same driving -- routed. No jump anywhere in the path. The window's own docstring claimed to handle "stretches of driving at different speeds", and this is what it did with two of them. Both halves of the neighbourhood were wrong, and each had already cost a corridor on its own: a window of FIXED WIDTH lets stillness crowd the evidence out. Eleven repeats per pose put the real steps either side of one outside it -- last commit's defect, patched then by falling back to the whole path. The legs nearest in time reach past any amount of stillness, so the fallback is not a special case any more; it is what the rule does. a MAJORITY inside one refuses a stretch that is not most of its surroundings. Three other moving legs of half the length is a stretch of driving. A relocalisation has no such company, and neither does a pair of them -- which the median, counting them among twenty, was more willing to bridge. Measured over 3000 generated recordings of drives, parks and relocalisations: a tenth as many real legs refused (74 against 744, and only these can cut a corridor -- they are the ones over twice the robot's radius), and no more relocalisations bridged (688 against 708). Eighteen shapes are the tests; the new one fails on the window, and the seventeen the window passed still pass.
…udged A leg is never one of its own vouchers, so asking three of a recording whose whole driving is three legs asks for a fourth that does not exist. The guard said `len(moving) < VOUCHES_NEEDED` and the pool excluded the leg itself, so the real requirement was four: a straight four-pose drive sampled every metre had every leg refused, `bridgeable` came back [0 0 0], and `plan` returned None over floor the robot had just driven. Five poses routed. Where the recording cannot supply as many vouchers as the rule wants, ALL of them have to agree instead -- the strictest such a recording can be held to. That stops at two: one other leg is not corroboration, and two displacements in an otherwise motionless recording would each be the other's only witness. Measured over the same 3000 generated recordings, letting a single leg decide bridges 765 of 5803 relocalisations against 729 for two, and saves no drive leg at all. The nineteenth shape, and a two-leg drive is now a stated refusal rather than an accident of arithmetic.
…st decode **Respelling an edge that joins nothing only breaks it.** A folded static is written in the moving stream's spelling so the chain closes -- and last round narrowed the spelling to the transforms that SURVIVE the fold, which leaves the map empty when the moving stream carried nothing but the stale copy. An empty majority is not a vote for "unslashed", and a vote taken over frames that have nothing to do with this edge is not a vote about it either: a recording whose tf and tf_static both said `/base -> /cam` came out saying `base -> cam`, and `MultiTBuffer` answered 3.0 before the fold and None after, on the only edge in the recording. Reproduced too with an unrelated `map -> other_robot` surviving. An edge is now respelled only when the moving stream mentions one of its frames, which is exactly when there is a chain to join. **"Cannot tell" is not "no objection".** The channel count breaks the depth tie, and a candidate whose sample will not decode has no count -- so it was treated as unobjectionable and won on its shorter name. There is no route by which the module could read a metre out of it: a `depth` stream with a truncated blob, which is what a killed writer leaves, beat a `camera_depth_image` beside it that reads as real DEPTH16. Unreadable now sorts last, below even the colour one.
…child **The read-only wrapper forwarded writable objects.** `limit`, `after`, `near`, `order_by` and a dozen more each return ANOTHER stream -- a view of the same table, with the same `append` on it -- so `store.streams['x'].limit(1).append(9.0, ts=2.0)` put a row in the operator's recording through a wrapper whose whole job is to stop that. `save(target)` is the same hole by another name: it appends every observation into the target's backend. Naming the writes on a surface that wide was never going to hold, so what a forwarded attribute RETURNS is wrapped as well. **A group is not its first process.** `_end_group` returned as soon as the child it launched was gone, so the SIGKILL pass never ran -- and the group is exactly where the things that ignore SIGTERM are. Measured: the child died on the TERM, its grandchild went on appending to a file every 0.2 s indefinitely, and `analyze_memory` reported EXECUTION_TIMEOUT. Both signals now go to the group, whose id is read once up front because the wait reaps the child and `getpgid` of a reaped pid raises.
Which frames the moving stream's spelling REACHES is not which frames it says. A mount hangs off a frame the moving stream says, and a camera hangs off the mount: `odom -> base` moving, `base -> /mount` and `/mount -> /cam` static. The first folded edge joins the moving stream and was respelled; the second joined only the FIRST, counted as joining nothing, and kept its slash. The tree came out holding `base -> mount` beside `/mount -> /cam`, and `MultiTBuffer` answered 6.0 for `odom -> cam` before the fold and None after. Two spellings of one frame is no chain at all, which is the thing this whole spelling business exists to prevent -- so the reach grows edge by edge from the moving stream's own frames until nothing new is joined. Last commit's fix for edges that join NOTHING stands; this is the other end of it.
A CLASS is callable, and `stream.data_type` is one. Wrapping everything a forwarded attribute returns -- which is how the read-only store stopped `limit` and `save` handing back writable objects -- turned the payload type into a function, so `np.array([o.data for o in stream], dtype=stream.data_type)` raised "Cannot interpret <function _read_only.<locals>.wrapped> as a data type". That is a read the analysis skill's own examples make. Types come back as themselves. Nothing can be written through one.
`views` is measured only where depth places the object. Without depth, `cluster_places` keeps the best frame of each location and drops the near-identical ones without counting them, so every `Place` carries the dataclass's 1 -- twelve frames of one object, and the results bar read "1 view". The sentence and the skill payload have refused to print that number on this branch since round 77: `_best_phrase` says "best match" instead, and `_place_metadata` omits `views` outright, both with comments saying a reader cannot tell a measured 1 from an unmeasured one. The `ClusterSummary` the CLIENT renders went on sending it, which is the copy that actually reaches a person. Zero is what the field's default already means, and the two places that render it -- the results bar and the tour card -- now leave the clause off rather than reading "0 views".
The `index.ndim == 2` test implied a single coordinate was allowed, and it was not: a `(3,)` array made a length-1 mask for a length-3 result and raised "boolean index did not match indexed array along axis 0". `pack_keys` indexes `[:, 0]` and takes nothing else, and both call sites pass (N, 3), so the branch was a trap rather than a feature -- a caller who believed it got an error about boolean masks instead of about its own shape. Latent, not an active-path defect: no shipped call reaches it. It is fixed because the cheapest thing to do with a false promise is stop making it.
`_channels` decoded `next(iter(stream))` and nothing else, and the ranking it feeds puts an unreadable candidate last -- so a depth stream whose FIRST blob will not decode, which is what a writer killed mid-frame leaves, scored below a colourised one and lost the role to it. Measured: twenty frames of real DEPTH16 with the first blob overwritten, and `detect_streams` picked `depth_color`, the three-channel RGB stream that makes `patch_world_position` raise "too many values to unpack" -- nineteen good frames of depth thrown away for one bad one, and the very outcome the test beside it exists to prevent. It reads on to the first sample that decodes, up to `CHANNEL_TRIES`. A stream where none of the first eight decode still has no opinion, and still sorts last.
…onfuses two frames refuses **Reading on until something decodes was not enough.** A stream whose shape CHANGES is not depth at all: eight mono frames in front of ninety-two RGB ones read as one channel, beat a real DEPTH16 stream, and `patch_world_position` raised "too many values to unpack" on the frame it was handed. The answer is now the count the samples AGREE on -- the first eight and the last -- and a disagreement is "cannot tell", which sorts last. One bad frame among good ones still does not condemn a stream, which is what the same function learnt last commit. **A frame has ONE parent in a tree, and respelling can hand it a second.** A recording that keeps `cam` and `/cam` as different frames says its camera is in two places: `odom -> cam` directly at 10, and `odom -> base -> /mount -> /cam` at 6. Folding respelled the static chain into the moving stream's convention, giving `cam` a second parent -- and then deleted `tf_static`, so the 6 was gone with nothing left saying it had been there. Measured: `odom -> /cam` read 6.0 before and None after. It refuses now, naming both parents. Refusing costs nothing: both values are still in the recording afterwards and the operator can see which one is wrong. Only a clash the FOLD creates counts -- a moving stream that already gives a frame two parents is its own problem, and a child whose moving edges were dropped as stale copies is not a clash at all.
Special methods are looked up on the TYPE, so `__getattr__` never sees them: `with store.streams['x'].limit(1) as s:` raised "'_ReadOnlyStream' object does not support the context manager protocol" on a stream that supports it perfectly well. The wrapper forwards them itself now, and `__enter__` hands back the WRAPPER rather than the inner stream, or the block body would be holding the writable one -- measured both: the rows read, and the append inside the block still refused.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Walk a recording in VR or on a phone, ask it a question out loud or by typing, and see where the answer is in the map with the photographs that back it.
Against
mainthis is the whole stack:dimos/teleop/memory_world(the module, the server and the viewer) plus thedimos/mapping/hyperspacework it was originally built on.Answers come from the recording's own CLIP/SigLIP embeddings. No Hyperspace at query time, no companion file. One recording, one
.db.find_in_memoryalways uses the embedding index. It used to prefer Hyperspace whenever that index happened to be loaded, so one recording could answer two different ways depending on a file's timing.clusters, which is what the viewer builds its results bar, its place stepping and its Navigate button from./navigateroutes to the embedding answer's places.text_aligned), so a stale stream of raw vision-tower tokens no longer reports search as ready and then fails every query.Verified live on
sf_office1_2/main.db:276 tests pass.
ruff checkclean under the repo config and under--isolated --select F821,F811,F841,F632,B023,F401.node --checkclean on all 17web/static/*.js, as script and as module.Known gaps, not addressed here:
siglip2-so400m-patch16-512gives 32x32 patches against the current 24x24 and is cached locally, if retrieval quality needs more.route.plan'swithin=reachablemask has no test.