You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
arm limits reports a compliant stop (a cable/loom that stiffens progressively) as a LimitVerdict.WALL, with nothing to distinguish it from a hard mechanical stop. On wrist_roll this is not hypothetical: the verdict does not replicate, and it came within
one commit of writing a permanent false fact into arm_spec.
A WALL is the only verdict that vouches for a mechanical limit
(LimitVerdict.vouches_for_a_wall, arm101/hardware/limits.py). It is what authorises a
re-zero. If a compliant end can pass as a WALL, then the seam can be parked in an arc that is not actually unreachable — which is the exact class of bug #43 exists to kill.
The evidence (follower /dev/ttyACM1, 2026-07-13)
wrist_roll, probed three times:
run
start
verdict
travel
arc
A
mid-travel
BOUNDED (walls raw 1700 / 1491)
3887
209
B
mid-travel, --threshold-joint wrist_roll=150
UNDETERMINED
3903
—
C
mid-travel, --threshold-joint wrist_roll=250
UNDETERMINED
3905
—
Travel replicates (3887 / 3903 / 3905). The END VERDICTS DO NOT.
Per-end detail from B and C:
threshold
low end
high end
150
WALL, peak 476
TORQUE_LIMITED, peak 172
250
WALL, peak 500
UNFIRABLE_THRESHOLD, peak 196
The load at the stop, on the same joint, at the same ends, has been observed at 172, 196, 272, 288, 476 and 500.
Ground truth from a human hand
The operator posed wrist_roll to each mechanical end by hand (torque off, at-rest reads):
end A
end B
human hand
raw 1769
raw 1429
the arm
raw 1700
raw 1491
The arm reached PAST the hand at both ends — 69 and 62 ticks. It squashes ~65-70 ticks
into each stop. Operator's words: "one side can be stopped a few ticks, and the other side is
not moving to the end."
A hard mechanical wall does not compress. A cable does. Corroborating: wrist_roll is the
only joint on the arm that does not saturate present_load against a stop — every other joint
shoves the full 500 cap into its walls.
Why this is dangerous, precisely
The unreachable arc is not a fixed quantity. It shrinks the harder something pushes:
gentle human hand -> arc ~340 ticks
the arm (capped) -> arc ~209 ticks
a harder push -> smaller still
A seam parked at the midpoint of the gentle arc (~raw 1599) sits inside travel the arm
reaches whenever it leans in a little harder (it got to 1491 and 1700). That is a seam inside
reachable travel — the precise failure #43/#35 are about.
Note the near-miss: MIN_EVICTABLE_ARC_TICKS is now 3 * ARC_MARGIN_TICKS = 300. The
hand-measured arc (340) passes that cutoff. Only the arm's more-aggressive 209 fails it. We
reached the right answer (soft-limit wrist_roll) by luck — the reasoning recorded in arm_spec._REZERO_REFUSED ("209 < 300") is sound only because the arm happened to be the
harder pusher. If a future run probes more gently (lower threshold => earlier stop => WIDER
arc), the classifier will happily offer a re-zero into a zone that is not unreachable.
A gentler probe over-estimates the arc, and over-estimating the arc is the dangerous
direction for seam eviction. Nothing in the code knows this today.
Proposed fix — a compliance probe
The discriminator is available and cheap: push twice, at two different strengths, and see if
the stop MOVES.
A hard stop is a position constraint: it lands on the same tick regardless of force.
A compliant stop is a force constraint: where it lands is a function of how hard you push.
Sketch:
Probe an end normally -> record stop tick t1 at push strength p1.
Retreat, re-probe the same end at a materially different push strength p2 (a different
contact threshold, and/or a different Torque_Limit cap -- the cap is the honest lever,
since the threshold only decides when we STOP, not how hard the servo pushes).
compliance_ticks = |t2 - t1|.
If compliance_ticks exceeds a (measured) tolerance, the end is COMPLIANT:
it must NOT vouch for a wall -> a new LimitVerdict (COMPLIANT_STOP?) or a flag on
the existing WallEnd;
it must NEVER authorise a re-zero (rezero_arc / classify.SeamRemedy.REZERO);
the reported arc must be the one from the HARDEST push seen (the conservative,
smallest arc), never the gentlest.
Open design questions (deliberately not settled here):
Is COMPLIANT_STOP a sixth LimitVerdict, or an attribute of WallEnd? It behaves like a WALL for bounds (the joint really is limited there) but like a LOWER BOUND for eviction (you cannot certify the arc). That asymmetry may not fit the current vouches_for_a_wall single-predicate design (limits.py:~196).
Cost: this doubles the probes per end. Acceptable for arm limits; probably not for arm explore.
Acceptance criteria
A repeat-probe at two push strengths is implemented and its delta recorded per end.
wrist_roll's ends are classified COMPLIANT on hardware (they move ~65-70 ticks).
shoulder_pan / elbow_flex / gripper low end are classified as HARD (they should not
move materially -- if they do, the tolerance is wrong, not the joints).
A COMPLIANT end can never produce SeamRemedy.REZERO, and there is a test that says so.
The arc reported for a joint with a compliant end is the one from the hardest push.
arm limits wrist_roll gives the same verdict twice in a row on hardware. Today it
does not, and that is the headline symptom.
Where the code is
what
where
the 5 verdicts + vouches_for_a_wall
arm101/hardware/limits.py
the WALL/TORQUE_LIMITED discriminator (loaded_run <= compliance AND free_run >= needed)
arm101/hardware/probe.py_rule_on_a_stop
wall_compliance() / free_run_needed() -- the existing (static!) compliance model
Note probe.wall_compliance() already encodes a static 100-tick "give" allowance. This
issue is that compliance is not static and not a constant -- it is a per-joint, per-end, measurable property, and on wrist_roll it is the dominant effect.
Reproduce
# from a MID-TRAVEL start (NOT sitting on an end -- a probe that starts on a wall has no# free approach and its result is degenerate; this bit me):
uv run arm101 arm limits wrist_roll --threshold-joint wrist_roll=150 --port /dev/ttyACM1 --apply --json
uv run arm101 arm limits wrist_roll --threshold-joint wrist_roll=250 --port /dev/ttyACM1 --apply --json
# -> different end verdicts, same travel.
Summary
arm limitsreports a compliant stop (a cable/loom that stiffens progressively) as aLimitVerdict.WALL, with nothing to distinguish it from a hard mechanical stop. Onwrist_rollthis is not hypothetical: the verdict does not replicate, and it came withinone commit of writing a permanent false fact into
arm_spec.A
WALLis the only verdict that vouches for a mechanical limit(
LimitVerdict.vouches_for_a_wall,arm101/hardware/limits.py). It is what authorises are-zero. If a compliant end can pass as a
WALL, then the seam can be parked in an arc thatis not actually unreachable — which is the exact class of bug #43 exists to kill.
The evidence (follower
/dev/ttyACM1, 2026-07-13)wrist_roll, probed three times:--threshold-joint wrist_roll=150--threshold-joint wrist_roll=250Travel replicates (3887 / 3903 / 3905). The END VERDICTS DO NOT.
Per-end detail from B and C:
The load at the stop, on the same joint, at the same ends, has been observed at
172, 196, 272, 288, 476 and 500.
Ground truth from a human hand
The operator posed
wrist_rollto each mechanical end by hand (torque off, at-rest reads):The arm reached PAST the hand at both ends — 69 and 62 ticks. It squashes ~65-70 ticks
into each stop. Operator's words: "one side can be stopped a few ticks, and the other side is
not moving to the end."
A hard mechanical wall does not compress. A cable does. Corroborating:
wrist_rollis theonly joint on the arm that does not saturate
present_loadagainst a stop — every other jointshoves the full 500 cap into its walls.
Why this is dangerous, precisely
The unreachable arc is not a fixed quantity. It shrinks the harder something pushes:
A seam parked at the midpoint of the gentle arc (~raw 1599) sits inside travel the arm
reaches whenever it leans in a little harder (it got to 1491 and 1700). That is a seam inside
reachable travel — the precise failure #43/#35 are about.
Note the near-miss:
MIN_EVICTABLE_ARC_TICKSis now3 * ARC_MARGIN_TICKS= 300. Thehand-measured arc (340) passes that cutoff. Only the arm's more-aggressive 209 fails it. We
reached the right answer (soft-limit
wrist_roll) by luck — the reasoning recorded inarm_spec._REZERO_REFUSED("209 < 300") is sound only because the arm happened to be theharder pusher. If a future run probes more gently (lower threshold => earlier stop => WIDER
arc), the classifier will happily offer a re-zero into a zone that is not unreachable.
A gentler probe over-estimates the arc, and over-estimating the arc is the dangerous
direction for seam eviction. Nothing in the code knows this today.
Proposed fix — a compliance probe
The discriminator is available and cheap: push twice, at two different strengths, and see if
the stop MOVES.
Sketch:
t1at push strengthp1.p2(a differentcontact threshold, and/or a different
Torque_Limitcap -- the cap is the honest lever,since the threshold only decides when we STOP, not how hard the servo pushes).
compliance_ticks = |t2 - t1|.compliance_ticksexceeds a (measured) tolerance, the end is COMPLIANT:LimitVerdict(COMPLIANT_STOP?) or a flag onthe existing
WallEnd;rezero_arc/classify.SeamRemedy.REZERO);smallest arc), never the gentlest.
Open design questions (deliberately not settled here):
COMPLIANT_STOPa sixthLimitVerdict, or an attribute ofWallEnd? It behaves like aWALLfor bounds (the joint really is limited there) but like aLOWER BOUNDforeviction (you cannot certify the arc). That asymmetry may not fit the current
vouches_for_a_wallsingle-predicate design (limits.py:~196)._PRESSING_EXCESS_LOADwas (see Every factory-offset joint is clamped exactly ON its encoder seam — #35 may affect all six #43 thread) -- measure the run-to-run spread of a KNOWN HARD stop
(
shoulder_pan,elbow_flex,gripperlow end all saturate cleanly) and put thetolerance above that noise.
arm limits; probably not forarm explore.Acceptance criteria
wrist_roll's ends are classified COMPLIANT on hardware (they move ~65-70 ticks).shoulder_pan/elbow_flex/gripperlow end are classified as HARD (they should notmove materially -- if they do, the tolerance is wrong, not the joints).
SeamRemedy.REZERO, and there is a test that says so.arm limits wrist_rollgives the same verdict twice in a row on hardware. Today itdoes not, and that is the headline symptom.
Where the code is
vouches_for_a_wallarm101/hardware/limits.pyloaded_run <= compliance AND free_run >= needed)arm101/hardware/probe.py_rule_on_a_stopwall_compliance()/free_run_needed()-- the existing (static!) compliance modelarm101/hardware/probe.pyMIN_EVICTABLE_ARC_TICKSarm101/hardware/classify.py_REZERO_REFUSED,REZERO_ARCS,SOFT_LIMITS,DEFAULT_CONTACT_THRESHOLDSarm101/hardware/arm_spec.pyarm101/cli/_commands/arm.py(arm limits)Torque_Limitcap (addr 48), the honest "push strength" leverarm101/hardware/gentle.py_CONTACT_TORQUE_LIMIT = 500Note
probe.wall_compliance()already encodes a static 100-tick "give" allowance. Thisissue is that compliance is not static and not a constant -- it is a per-joint, per-end,
measurable property, and on
wrist_rollit is the dominant effect.Reproduce
Depends on / relates to
wrist_roll's entire characterisation changed twice in one day.wrist_roll(currently refused; must STAY refused).arm explorewould build a grid from these bounds.