Buckets:
Method
One method, two regimes. A demonstration graph built from the training demos is the only model of "where the arm should be"; a small trained network turns a window of joint states into a distribution over its nodes. At t=0 that distribution is used once, to rewind an out-of- distribution start pose. At t>0 it is filtered over time and used to decide when the rollout has left the demonstrated manifold and where to put it back.
Nothing here is policy-specific: the graph and its head are trained once per suite from demo data alone, and the same artifacts drive both a frozen StableVLA and a frozen GR00T-N1.7 with no retraining of either policy.
1. The demonstration graph
Nodes. Raw demo frames are coarsened COARSEN = 5 frames to a node, with a forced split at every
gripper-state change (onf/graph/nodes.py). Each node carries its joint configuration q, finite-
differenced velocity, gripper flag, and three labels used everywhere downstream:
| label | meaning |
|---|---|
task_id |
which base task the demo belongs to — the task lane |
owner |
which demo (strand) it came from |
phase |
position along the episode, arange(T)/(T-1), so 0.0 = first frame, 1.0 = last |
The long suite's graph has 28,476 nodes over 500 demos / 10 tasks.
Edges (onf/graph/edges.py, schema.RELATIONS): temporal successors at dilations
next1, next2, next4, next8, next16 (plus their reverses), sibling (kNN across demos of the same
task, degree K_SIBLING = 8), and align (kNN within a phase bin, degree K_ALIGN = 4).
Head (onf/graph/gnn.py, trained by onf/graph/train.py → g_head.npz). Input is a window of
HIST_H = 8 consecutive joint states. A query encoder seeds node states with a joint-space kernel
(bandwidth KERNEL_BW = 0.15, sparsified to SEED_TOPK = 256 nodes), LAYERS = 3 rounds of
relation-typed message passing follow, and a logit head scores every node. Nodes never reached by
message passing get -inf, which is the head's reachability mask.
The objective is phase-primary: cross-entropy on the phase-bin marginal p(T|Q)
(PHASE_CE_W = 1.0) plus a phase-expectation regression (PHASE_EXPECT_W = 0.5), with node identity
only auxiliary (NODE_W = 0.1). This ordering is deliberate — which demo a window came from is
close to unlearnable and barely matters (same-task, same-phase demos sit ~0.05–0.6 rad apart), while
how far along is both learnable and decisive (a phase error of 0.5 costs ~0.5 rad of target error).
Training queries also include an ENTRY_STATIC class: windows of the form repeat(q0, 8), i.e.
identically zero velocity. Ordinary training windows are consecutive demo frames and always carry
some velocity, so without this class the t=0 query shape never appears in training at all.
2. t = 0 — entry
WHEN. Purely geometric, no learning: at episode start compute qerr0 = ||q_0 − q*||, and act only
if qerr0 > JQ_GATE = 0.08 rad. An unperturbed start is left completely untouched.
WHERE. The head's distribution is masked down to the entry stratum
task lane ∩ phase ≤ JQ_GRAPH_ENTRY_BAND (0.05) ∩ reachable (logit > −inf)
and the survivors are averaged uniformly in joint space (EucRawReadout):
q* = Σ_v w_v · q_v with w_v = 1/n. On the long graph this is ~229 of 28,476 nodes.
So at t=0 the trained head contributes exactly one thing — reachability — and the mask, not the
weighting, determines the answer. This was measured directly: reading the head's posterior over the
same masked pool instead of averaging uniformly moves q* by 0.124 rad (larger than the gate itself)
and yet does not improve success rate. The fixed rule is kept because it is simpler and does not
depend on which head is loaded.
How it moves. A joint-space PD servo (onf/recovery/controller.py, JQ_MODE=pd, JQ_KP = 80)
drives the arm to q* with tolerance JQ_EPS = 0.03 rad and a hard budget of JQ_MAX = 60 control
steps, then hands control to the frozen policy. The target is a segment (SEG_K = 8 frames), not
a point, so the policy receives a state that is in-distribution in both q and qdot.
JQ_POSTURE=target is load-bearing. The operational-space controller keeps a null-space posture
reference. If it is left at its default the servo lands correctly and then the null-space torque drags
the arm back toward the perturbed pose within ~2 s of handoff, undoing the recovery. Pinning the
reference to the pose the servo reached is what makes the whole regime work.
3. t > 0 — sentinel
A per-worker GraphTracker (onf/graph/track.py) runs a belief filter over the same graph, one
check every SN_GRAPH_STRIDE = 1 window (matching the 8-raw-frame training window):
b_t ∝ [ (1 − ε) · P b_{t−1} + ε · L_t ] · L_t
L_t = softmax(logits)— the same trained head as t=0, no separate model.Pis built from the graph's own relations, restricted toTRACK_KERNEL_RELATIONS = (next1, next2, next4, next8, next16, sibling). Advances are mixed overTRACK_ADVANCE_SET = (0,1,2,4,6,8,12,16)raw frames because a check advances ~1.65 nodes, which integer node hops cannot express, and rollouts run slower than demos.alignand backward relations are deliberately excluded: the arm does not change task mid-episode, and a backward jump must pay transition cost.- The restart term
ε · L_trestarts toward the current observation, not toward uniform — a uniform restart puts mass on impossible phases and discards the aliasing suppression being bought.
Why filter at all: the retriever is memoryless, and long tasks are two-object ("put both X and Y"),
so reaching for object 2 looks kinematically like reaching for object 1. Over 1,372 measured
consecutive checks only 67% of the memoryless phase transitions are physically plausible
(|Δphase| ≤ 0.05); 8.8% teleport by >0.3 and 8.0% jump backwards by >0.1.
WHEN (FilterRule, onf/sentinel/when.py). Two e-processes accumulated by the tracker — a
basin test (is the arm inside the certified radius of the demo manifold?) and a progress test
(is phase still advancing?) — each fire when log E ≥ −log(α_half) (Ville's inequality). The rule
fires on their union, subject to three gates:
α_half = α / 2 / max_interventions(α =TRACK_ALPHA_EVIDENCE= 0.05). ResettingE_tafter a fire refunds spent α, so the per-episode false-alarm guarantee only holds if the budget is split across the allowed fires. Without this split the certified claim is simply false.- The progress test is suppressed once
phase_hat ≥ 1 − 1/NBINS_ALIGN: a task that legitimately finished has stopped advancing, and that is not a stall. - No fire while the gripper is closed — interrupting mid-grasp drops the object. The evidence bookkeeping keeps running; only the fire is suppressed.
WHERE (BasinReadout). The belief b_t is projected onto the basin geometry fitted per
(task, phase-bin) from held-out clean demo replays: certified radius r = p95 and KDE bandwidth
h = p50 of leave-one-demo-out nearest-neighbour distances. The same PD servo then runs to that
target.
Artifacts are fit offline by scripts/build_sentinel_artifacts.py into g_track.npz
(π, β, leak, the three null banks, basin_r, basin_h). The file carries a graph_hash stamp
and is refused on mismatch: the advance operator is node-id positional, so a foreign file would
advance the belief along the wrong strands with no shape error and no exception.
4. Constants: chosen vs derived
| constant | value | how |
|---|---|---|
JQ_GATE |
0.08 rad | CHOSEN |
JQ_GRAPH_ENTRY_BAND |
0.05 phase | CHOSEN (one NBINS_ALIGN bin) |
JQ_KP / JQ_EPS / JQ_MAX |
80 / 0.03 / 60 | CHOSEN |
HIST_H, SEG_K |
8, 8 | CHOSEN — one policy action chunk |
COARSEN |
5 | CHOSEN; splits forced at gripper changes |
TRACK_ALPHA_EVIDENCE/PROGRESS |
0.05 | CHOSEN (false-alarm budget) |
TRACK_LAM |
0.8 | CHOSEN |
α_half |
α/2/max_interventions |
DERIVED (union bound over fires) |
BASIN_RADIUS_QUANTILE / BANDWIDTH |
p95 / p50 | DERIVED from LODO demo replays |
π, β, leak |
per suite | DERIVED — fit by build_sentinel_artifacts.py |
TRACK_ADVANCE_SET upper end |
16 raw frames | DERIVED (~2× nominal advance rate) |
5. Results
Every number below is on LIBERO-Plus. Nothing else in this repo carries measured numbers.
| cell | StableVLA | GR00T-N1.7 |
|---|---|---|
t=0 entry, long × Robot_Initial_States (n=393) |
378/393 = 96.2% | 353/393 = 89.8% |
t>0 sentinel, long × Objects_Layout (n=312) |
not measured | 208/312 = 66.7% |
Read with the following caveats, all of which matter:
- The 66.7% cell has no same-session
basebaseline. The nearest comparator (204/312) was run four days earlier; the difference is +1.3 pp. GR00T-N1.7 is flow-matching and stochastic — ~11.2% episode churn, ±2.4 pp on n≈393 — so +1.3 pp is inside noise, and cross-session GR00T comparisons are not valid at all. Only same-session arms may be compared. - StableVLA is deterministic (0 episode churn), so its arms can be compared paired and exactly, even across sessions.
- On GR00T at Robot-Init, a fixed-home baseline (94.7%) still beats this method (89.8%). The graph-based entry target wins on StableVLA but not on GR00T.
Not measured
sentinelhas exactly one measured cell (GR00T,long× Objects_Layout) and no full-suite number on StableVLA.configs/sr_ladder.yamlnevertheless makes it the default for all six t>0 axes — that is an extrapolation beyond what has been measured, stated here rather than hidden.- Only the
longsuite currently has a trainedg_head.npzand a fittedg_track.npz.object,spatialandgoalhaveg_nodes.npz/g_edges.npzonly, so the 4-suite benchmark is complete in code but cannot run on three of four suites until those artifacts are trained (seedocs/usage.md). ENTRY_STATIC's effect on the t>0 regime has never been ablated. It is retained because the shippedg_head.npzwas trained with it.src/onf/field/(ONFField) is a training-time dependency only — it supplies the cleanliness weighting inonf/graph/train.py. It is on no inference path in either regime.
Xet Storage Details
- Size:
- 10.5 kB
- Xet hash:
- 689c50d651eb808a7959cdd0597166cd0194cdb5c97b14c5c0548a3abe9d062c
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.