Research blog · November 11, 2026
Repairing Robot Policies with Weight Space Programming
Frontier Models can restore task success for vision-based behavior cloning policies under distribution shifts, directly in weight space
1CITEC, Bielefeld University 2Fraunhofer IOSB-INA *Equal contribution
TL;DR
- Recent work (“Sparks of Weight-Space Programming” by Takehi et al. [1]) has explored language models that natively read and write the weights of other neural networks. We bring this idea to robot learning and find that LLMs are surprisingly good at reasoning about why closed-loop, vision-based policies fail.
- We train a robot arm to pick up a cube and put it in a tray from camera images, by imitating demonstrations, and then break it with small distribution shifts.
- To diagnose the failures, the LLMs find the neurons the trained policy never uses and probe the network with tools from mechanistic interpretability.
- To fix them, they write small programs into those unused neurons: controllers that switch on only when needed, such as lowering the cube once the wrist camera sees the tray below.
- In doing so, the LLMs give the policy qualitatively new capabilities. When the tray's location is no longer static, three of the LLMs wire a readout from the cameras into the network that tells the arm where to carry the cube: spatial generalization that was not present in the original policy.
- Across five experiments, the best edits raise task success from 0–5% to between 33% and 95%. And the skill seems to grow with model capability: the newest model we tested (Fable 5.2) does best in all experiments.
- All of this works without gradient-based optimization or fitting, and without a single new demonstration, using only a fixed budget of test runs.
- Throughout the history of robot learning, a failing policy has meant more data: more demonstrations, more robot time, another round of training. This may be changing: robot policies could soon be debugged and repaired natively in weight space, by LLMs that reason about why the robot fails.
Teaching your robot new skills has never been easier: low-cost robots have boomed, open datasets keep growing, and robot foundation models are closer than ever to working out of the box.
So say you collected some demonstrations, fine-tuned your favourite VLA and deployed it on your 3D-printed robot of choice. Then, in certain states, it just does not work. It tries to grasp something, fails and loops. It stalls halfway through the task. Something as simple as a change in lighting throws it off. What do you do?
For most roboticists, the answer is more data (e.g. DAgger [2]) and another round of training. We skip both and instead present early results showing that a frontier LLM can identify the failure modes that appear in closed loop and iterate on fixes, entirely in weight space.
Behaviour Cloning and Its Limits
Learning robust behaviour by imitating humans is a long-standing goal in robotics. Behaviour cloning is attractive because it is simple: it turns a control problem into a supervised learning problem.
For the same reason, it is also hard: it is fundamentally limited by distribution shift. The policy learns from the state distribution of its demonstrator, but when rolled out, it visits its own. Each small mistake takes it into states the demonstrator never saw, where bigger mistakes become more likely. This is known as compounding error [3], and it happens even in the very environment the policy was trained in.1 It only gets worse under the obvious shifts, like a visual distractor or a new object.
Yet much of what a policy has learned often stays useful under a shift, which suggests that a fix may be small and surgical. But where would we make it? To us, a trained policy is a black box. The only lever we have is the data it learns from.
Opening the Black Box
LLMs already sit at the heart of robot learning. Vision-language models are the backbone of today's robot foundation models (OpenVLA [4], π0 [5]), and LLMs write robot code (Code as Policies [6]) or even control robots directly [7]. But none of them can look inside a trained policy and tell us why it fails.
That is starting to change. In Sparks of Weight-Space Programming [1], LLMs read and write a network's weights directly, without an optimizer, which the authors call weight space programming. In one of many striking demonstrations, GPT-6 Astra is handed a policy whose car gets stuck on a ramp in every episode. From the raw weights and one recorded episode, it explains why, then rewrites 3.5% of the weights in a single reply. Now the car succeeds every time. LLMs, it seems, can reason in weight space about how a policy behaves. While recent work applies this to specific aspects of a robot foundation model, such as making π0.5 move faster [8] or follow paraphrased instructions more reliably [9], the rest of this blog is dedicated to studying whether LLMs can rescue policies that no longer work at all, by finding out in closed loop why they fail and fixing them in weight space.
The Setup
To study this in a controllable and reproducible way, we test it on a standard robotic manipulation task in simulation: cube pick-and-place. Our suite builds on OCBench [10], with one slight tweak. A tray now marks where the cube goes, which gives us a goal we can move later to introduce distribution shift.
To train a policy, we need demonstrations. Here we script a near-optimal expert that collects them for us, using privileged information such as the true position of the cube. This expert is memoryless and only reacts to the current state of the world; it is Markov.
The policy is a neural network that sees three camera images and the robot's own state. A pretrained ResNet [11] compresses each image into 256 numbers, and a small network with three layers of 256 ReLU neurons maps everything to target angles for the joints and a command for the gripper. Like the expert, the policy only sees the present, so it is Markov as well, and in principle it can imitate the expert perfectly. The difference lies in what they see. The expert reads the simulator's true state, while the policy has to infer it from images.
If we simply train the policy to reproduce the expert's actions, it fails almost completely at test time. Ironically, the expert is too good; it never makes a mistake, so its data never shows how to recover from one, and the policy runs straight into the compounding error described above. The fix may seem counterintuitive: we run the expert with noise added to its actions, so its demonstrations also visit states slightly off its own path, and the recorded actions show how it steers back. The policy learns to recover from its own small mistakes, which keeps it in-distribution even in closed loop. With this recipe, known as DART [12], our policy succeeds in 95.8% of held-out test episodes after training on 8,426 demonstrations.2
This trained policy is where every experiment starts. We drop it into a situation it was never trained for, watch it fail, and ask an LLM to restore its performance. The LLM can act only by changing the 335,111 parameters of the MLP on top of the frozen ResNet, numbers that come with no labels and no explanation. To make that a fair and repeatable test, a harness controls everything else, from what the LLM gets to see to how its final edit is scored.
The harness in detail
Five Ways to Break a Policy
To find out whether an LLM can repair a failing policy, we first need one that fails. We move the goal, once or in every episode. We recolour the cube, declare a zone off-limits, and swap the expert for one that behaves like a human. Every change collapses the original policy from 96% to near 0% task success. Then an LLM gets to work. Some frontier models even found a loophole in our success test and used it to score. In each experiment we present the best-performing LLM without such an exploit. The exploits are covered later in the case studies.
Experiment 1 of 5 · Spatial Adaptation
Distribution shift. The base policy is trained with the tray at position A. We move the tray by 21 cm to position B, where the LLM edits and tests the policy. There the base policy still lifts the cube, then hovers between the old and the new tray, with 0.2% task success. To test generalization, we also test on a new position C, 24 cm from position A in the opposite direction, which the LLM never sees.
In Opus's own words
- t4After lifting, the policy carries the cube to roughly (0.52, 0.12, 0.22) and hovers there indefinitely. […]
- t23[…] Swapping in the hover frame's front features (−1.2 cm/step) or proprioception (−1.0 cm/step) weakens it, and front+side+proprio together cancel it to ≈0. So the stall comes from absolute-position cues (front-camera arm appearance and proprioception) that implicitly assume the old container position.
- t30The edit overshoots badly. The arm carries the cube to x ≈ −0.10, y ≈ 0.5–0.8, far beyond the container and behind the base […]
- t69[…] My earlier (0.57, 0.20) was 7 cm off in x, which alone would have doomed any fixed-target edit.
- t112Under v4, the xy velocity at the hover spot is essentially zero (0.24, 0.01): this is a stable stall, about 7 cm short of T1 in y.
d is the shift of the inputs (arm readings, front and side cameras), row 785 of fc0.W reads the gripper closuredef build_shift(d):
W0=load_original('fc0.W'); b0=load_original('fc0.b'); mu=load_original('obs_mean'); sd=load_original('obs_std')
dn=np.where(np.isfinite(sd), d/sd, 0).astype(np.float32)
w=dn@W0
W=W0.copy(); W[785]+=sd[785]*w; b=b0+mu[785]*w
⋮
d4=comp(gpan=0.8,gi=1.0,ge=0.5,gjo=-0.3)
⋮
write_shift(d4); np.save(os.path.join(WORK,'d4.npy'),d4)In Opus's own words
- t115The proprioceptive cues barely steer y in the band: even an extra 0.2 rad of pan shift adds only +0.2 mm/step, and eff_y does nothing. […]
- t115A different repair: add a small hand-designed proportional controller on the shoulder_pan joint inside the network. […]
- t116[…] many units never activate (178 in fc0, 201 in fc1, 143 in fc2), so I can use some of them as a dedicated pathway.
- t118[…] The pan change is exactly zero while open, while closed on nothing, and while low, and on high carry it equals the clipped kp·e (correlation 1.0).
- t123The pan controller works: final pan is about -1.45 (the T1 angle) in every failed episode. The remaining error is reach. […]
fc0.W read the base angle, the height and the gripper closure; column 0 of out.W drives shoulder_panTH1=-1.447 ⋮ W0[768,p0]=-kp*os_[768]; b0[p0]=kp*(TH1-om_[768])+c0 W0[768,n0]=+kp*os_[768]; b0[n0]=-kp*(TH1-om_[768])+c0 W0[785,gG]=-10*os_[785]; b0[gG]=10*(0.9-om_[785]) W0[785,gE]=+10*os_[785]; b0[gE]=10*(om_[785]-1.3) W0[782,gZ]=-5*os_[782]; b0[gZ]=5*(zthr-om_[782]) q1,r1=u1 W1[p0,q1]=1; W1[[gG,gE,gZ],q1]=-M; b1[q1]=-c0 ⋮ Wo[a1,0]=1; Wo[a2,0]=-1; Wo[c1_,0]=-1; Wo[c2_,0]=1
In Opus's own words
- t123[…] The cube stalls either short (x ≈ 0.43–0.47) or long (x ≈ 0.53–0.66), hovering at z ≈ 0.14–0.21 without descending. So I need a second controller for x along the pan ray.
- t129The successful placements end with cube xy around (0.497, 0.208) […]
- t130[…] Episode 27 sits at x = 0.537 with the pan correct and never descends: an x error with no descent trigger.
- t133[…] Nothing changes while the gripper is open, so approach and grasp are untouched.
- t13577/100: the reach and descent fixes solved almost all carry failures (only 1 lifted-not-placed left). […]
UX is the demonstrations' outward motion, UD their lowering motion (shoulder_lift, elbow, wrist_1); sco reads the gripper's x and y as reachUX=np.array([0,.229,-.201,-.020,0,0,0]); UX/=np.linalg.norm(UX[1:4]) UD=np.array([0,.195,.127,-.122,0,0,0]) ⋮ w,b=lin(sco,s1,kx); setcol(7,w,b+10); setcol(8,-w,-b+10) # Px0,Nx0 w,b=lin({768:1.},TH1,1/tolp); setcol(9,w,b); setcol(10,-w,-b) # Ap,Bp w,b=lin(sco,s1,1/tols); setcol(12,w,b); setcol(13,-w,-b) # As,Bs ⋮ u=4; T['fc1.W'][:,u]=0; T['fc1.W'][[9,10,12,13],u]=-1; T['fc1.W'][[3,5],u]=-10; T['fc1.b'][u]=1; T['fc2.W'][u,:]=0 ⋮ T['fc2.W'][:,20]=0; T['fc2.W'][4,20]=1; T['fc2.b'][20]=0; T['out.W'][20,:]=cd*UD
In Opus's own words
- t138The wrist view in episode 25 is dominated by the container: a large brown tray roughly in the centre, with the cube at the bottom-right edge next to the right finger. […]
- t148[…] The 2× shift fixed exactly the cubes with low or mid y, near T0 […]. But it created new hovers for cubes at y > 0.3 […]. So the image-based cube estimate has a position-dependent error that a constant shift can only trade around […]
- t169[…] So the injection that pulls the gripper out of the hover stall keeps biasing it during the final descent.
- t181The near-cube inhibitor does what it should. Gate L stays on in the stall frames it was built for (0.99 → 0.96) and is mostly removed in the harmful far-from-cube frames (0.99 → 0.11). […]
- t18392/100, a big jump. Both gate fixes worked. […]
fc0.W read the gripper's x, y and height, 785 its closure, 0–767 the three cameras; D2L, D2H: the camera shift's mean effect on the third layer w,b=lin(ym,0.17,-kr); setcol(free0[0],w,b) # RyL: y>.17
w,b=lin(xm,0.50-.425,-kr); setcol(free0[1],w,b) # RxL: x>.50
w,b=lin(ym,-0.32,kr); setcol(free0[2],w,b) # RnL: y<-.32
w,b=lin({785:1.},0.15,-10.); setcol(free0[3],w,b) # C: g_raw>.15
⋮
T['fc2.W'][gL,:]=sL*D2L; T['fc2.W'][gH,:]=-sH*D2H
⋮
W0[:,uz]=0; W0[782,uz]=-os_[782]/(10*w); b0[uz]=(10*zt-om[782])/(10*w)
W1[uz,:]=0; W1[uz,6]=-1.0; W1[uz,7]=-1.0
⋮
W0[:,uf]=0; W0[:768,uf]=-dAn/55.0; b0[uf]=1395.0/55.0
W1[uf,:]=0; W1[uf,6]=-1.0In Opus's own words
- t281[…] I plan a recovery reflex for that closed-empty state: lift while still closed, then reopen once the gripper is high, so the policy approaches again. The two physical gates (gripper fully closed, then height) give the hysteresis that the memoryless policy cannot provide.
- t283[…] Raw gripper closure never exceeds 1.01 in any frame of a successful episode, while closed-empty frames have raw closure between 2 and 3. […]
- t284The contact guard works: carry frames above 14 cm have contact below 0.1 in only 3 of 3661 frames. […]
- t293[…] In episode 76 the reflex opens the gripper only to g ≈ 1. Once the gripper sinks below 14 cm the Open gate dies, and the policy re-closes on the way down. […]
- t297Pass 17 solved all 100 diagnostic episodes. Making Open also lift completed the recovery. […]
GI, ZI, CI are the inputs gripper closure, height and pad contact; out.W columns 1–3 move shoulder_lift, elbow, wrist_1, column 6 the gripperraw_unit(40,{GI:2.0},-3.2) # E
raw_unit(41,{ZI:2.5},-25*0.13) # Zh (z=x/10)
raw_unit(43,{ZI:-2.5},25*0.14) # Lo
raw_unit(44,{GI:2.0},-0.3) # G
⋮
W1[40,26]=1; W1[41,26]=-10
W1[44,40]=1; W1[40,40]=5; W1[46,40]=-10; W1[43,40]=-50
⋮
Wo[32,1:4]=-1.1*np.array([.195,.127,-.122]); Wo[36,6]=-3
⋮
raw_unit(41,{ZI:2.5},-25*0.16); W1[41,26]=-10
⋮
Wo=load('out.W'); Wo[36,1:4]=-0.5*np.array([.195,.127,-.122]); save_edit('out.W',Wo)94.6% task success in 500 unseen episodes at position B (original 0.2%), 0.0% at position C. Its controllers steer to fixed numbers for the tray at position B, and Opus predicted the result. It had checked whether the wrist camera could find the tray anywhere, found it could not, and kept the fixed target. Experiment 2 asks for the general version. Fable 5.2 scored higher, through a loophole much like the one in Memory in a Memoryless Network.
For [position C], which I never saw, I expect no gain over the original. Both will most likely be near 0%, because my edit carries the cube to [position B].
Total edits: 4,674 weights written (1.4% of the network) and 39,512 set to zero (11.8%), in 320 reasoning turns.
How about other LLMs?
All five LLMs got the same task, tools, budget and rules: GPT-6 Astra, Sonnet 5.5, Fable 5.1, Opus 5.5 and Fable 5.2. Task success over the rounds, and in the unseen episodes at position B (right).
Experiment 2 of 5 · Spatial Generalization
Distribution shift. Now the tray is placed at random for each episode, anywhere in a rectangle of 27 cm × 63 cm. A fixed solution like the one in experiment 1, which steers to one known spot, is no longer enough: the policy has to look where the tray is in every episode and carry the cube there. Unedited, it carries the cube toward the tray's old spot and hovers: 5.0% task success.
In Opus's own words
- t5The under-reaction is clean: placement ≈ fixed point + 0.19·Δcx + 0.41·Δcy, independent of the cube start.
- t49This is the key finding. Swapping in demo front/side features, where the tray sits at c0, makes the policy move strongly toward c0 (cos +0.77). So the carry target is set visually, by where the tray appears in the front/side images relative to the gripper; proprioception barely matters.
- t56Pass 2 broke grasping: 40 episodes never touched the cube and 19 touched without lifting. The amplification is applied in every phase, so readout noise disturbs the reach.
- t98Big finding: 236 of the 256 fc0 units are dead, i.e. never active in demos or pass 0; only ~20 live units carry everything.
- t111Pass 3 used 3.7 and 1.6, about 5× too strong, hence the overshoot and runaway.
fc0.W read the front and side cameras, row 786 the pad contactfor (k_,(a_,u_,w_,sg)) in enumerate([(ax_,ux_m,wx,1),(ax_,ux_m,wx,-1),(ay_,uy_m,wy,1),(ay_,uy_m,wy,-1)]):
j=U[k_]; W0n[:,j]=0; W0n[:512,j]=sg*a_*u_; W0n[786,j]=Bc
b0n[j]=-sg*a_*(u_@muD)-Bc*thr_n
W1n[j,:]=sg*0.5*(w_@W1)
⋮
E2=build_steer(0.75,0.30)In Opus's own words
- t114The more striking finding is ten failures hovering within 5 cm of the tray, including episode 11 at only 1.8 cm, that never descend.
- t116Both cameras vote on descent. […] The front/side views, where the arm is not at the trained location, veto the descent even when the wrist sees the tray beneath.
- t131The wrist "tray beneath" detector works well. […] On held-out episodes its AUC is 0.98, and since distance is yaw-invariant the detector needs no rotation handling.
- t139The gate turns off because these episodes hover at 3–4 cm, where the score is only about 0.5–0.7.
fc0.W read the wrist camera; v_desc moves shoulder lift, elbow and wrist 1v_desc=0.1*np.array([0,1,3.36,-2.18,0,0,0]) ⋮ W0_[:,ua]=0; W0_[512:768,ua]=g*dn W0_[786,ua]=Kc*os_[786]; b0_[ua]=-g*(midpt@dn)-g*th+Kc*(om[786]-1.0) W1_[:,ub]=0; W1_[ua,ub]=1.0; b1_[ub]=0 W2_[:,uc]=0; W2_[ub,uc]=1.0; b2_[uc]=0 Wo_[uc,:]=lam*v_desc ⋮ E=build_desc(g=3.0,th=0.2,lam=2.5)
In Opus's own words
- t157At step 90 the y error is −0.74, so pan is saturated at −0.96. The arm whips sideways about 27 cm in 15 steps, loses the cube, and the network reacts erratically to the out-of-distribution motion.
- t173Found the failure. The servo's x and y drives are positive in 97–99% of carry frames because the readout is biased (median perceived x error +0.19 m), so the clipped servo pushes in a constant direction instead of closing a loop.
- t178So I'll inject the steering directly at the output: the gated readout feeds dead relay units into out.W as an exact world-x/y motion. That is a feed-forward target shift of K·s, not a servo on s − ee, so readout bias can't create a constant runaway drive.
- t18322/100, a new best.
vx, vy are the joint mixes for 1 cm in world x and y for j,(u,sg) in zip([1,2,3,4],[(ux,1),(ux,-1),(uy,1),(uy,-1)]):
W0[:,j]=0; W0[:512,j]=sg*g*u*sd[:512] # input normalized: x0=(x-mu)/sd -> weight on x0 = g*u*sd
W0[786,j]=Bc; b0[j]=sg*g*(mu[:512]-Z['muD'])@u + gate
⋮
for k,(v,V,sg) in enumerate([(vx,Vx,1),(vx,Vx,-1),(vy,Vy,1),(vy,Vy,-1)]):
Wo[r2[k]]=sg*(7*V/10)/(2*g)*v
⋮
build_ff(Bc=20.)In Opus's own words
- t273Pass 15 gave 24/100, so the y-gain change did not help. Passes 12–15 now sit in a 24–27 plateau, within noise of each other.
- t305The hover at +y is a force balance. My added pan action is exactly cancelled by the base policy's pull back toward its training target (ep 38: base −0.155, edit +0.154).
- t307The deeper problem is that my pathway is pure feedforward: it applies a push proportional to the readout s, and the hover settles where that push balances the base policy's pull. That pull varies a lot by region (steep at +y), so any fixed gain under-reaches in some regions and overshoots in others. The readout also contains positive arm-position feedback of about +1.75 drive per metre of gripper position.
- t309Pass 20 reached 31/100, the best so far (+27/−2 against the original). The negative position feedback is the first change since pass 14 that breaks the plateau.
fc0.W read the effector x and y, f2 are the steering copy-unitsfor idx,(P,N),off in [(780,(1,2),0.425),(781,(3,4),0.0)]:
# ee_m = off + (x0*sd+mu)/10 ; add -L*(ee_m-ebar) to P, + to N
k=L_*sd[idx]/10; c_=L_*(off+mu[idx]/10-ebar[idx])
W0[idx,P]-=k; b0[P]-=c_; W0[idx,N]+=k; b0[N]+=c_
⋮
for n in range(20): Wo[f2[n]]*=1.533.2% task success in 500 unseen episodes (original 5.0%). Opus's edit reads where the tray is from the cameras and carries the cube there, so it now succeeds with trays the original never reaches. Its estimate of the tray is only accurate to a few centimetres, so with the tray far from its training position the cube often stays hovering next to it. Three other LLMs scored higher, all through loopholes of the success test: both Fable models, with 93.0% and 99.0%, and, much less successfully, Sonnet 5.5 with 46.0% (Memory in a Memoryless Network).
The readout scatters by about 5–7 cm per coordinate.
The arm sometimes hovers within a few cm of the container without the descent trigger firing.
Total edits: 2,522 weights written (0.75% of the network) and 30,082 set to zero (9.0%), in 322 reasoning turns.
How about other LLMs?
All five LLMs got the same task, tools, budget and rules: GPT-6 Astra, Sonnet 5.5, Fable 5.1, Opus 5.5 and Fable 5.2. Task success over the rounds, and in the unseen episodes (right). The scores of Sonnet 5.5 and both Fable models come from loopholes (†); Fable 5.1's is described in Memory in a Memoryless Network.
Experiment 3 of 5 · Object Generalization
Distribution shift. The policy was trained on a blue cube; now it is green. The camera encoder is frozen, so the LLM cannot change how the cameras see green, only what the policy network does with it. In 60% of episodes the gripper never touches the cube and hovers about 15 cm above it. The LLM only sees green during the rounds; the unseen episodes add six more colours (red, yellow, orange, purple, magenta and white) to test generalization.
In Fable's own words
- t5Key finding: the arm gets within ~2.6 cm horizontally of the green cube (median) but does not descend […]
- t7Correcting just the wrist block's mean in hover frames restores most of the descend action […] So the "cube is under the gripper → descend" signal lives in colour-sensitive wrist features.
- t20Important observation: the "blue-specific" wrist features do respond to the green cube in the same direction, only much weaker (roughly 15–30% of the blue response […] So the green cube is seen as a faint blue cube.
- t40Offline, the rank-one translation (scale 1.0) restores the full descent command on green hover frames (−0.0185 m/step vs −0.020 for blue) […]
- t42Pass 1 gives 9/100 (from 0), with far more contact and lifts, so the wrist-pattern repair helps but is not enough.
fc0.W read the wrist camera; uG is the green "cube below" direction, D1 the blue hover mean minus the green onerG_all, fG_all = green_dir(np.ones(len(Hr),bool)); uG = rG_all/(rG_all@rG_all)
D1 = hovB - hovG
def edit_translate(P, u, m0, Delta, scale=1.0):
Q = dict(P); Wn = P['fc0.W'].astype(np.float64).copy(); bn = P['fc0.b'].astype(np.float64).copy()
t = scale*(Delta @ Wn[512:768]) # effect of the pattern on fc0 pre-activations
Wn[512:768] += np.outer(u, t); bn -= (m0@u)*t
⋮
P1 = edit_translate(P0, uG, fG_all, D1, 1.0)
save_edit('fc0.W', P1['fc0.W']); save_edit('fc0.b', P1['fc0.b'])In Fable's own words
- t43[…] (c) it grasps and carries the cube above the container but never puts it down (lifted_not_placed, 21 episodes).
- t48The wrist block alone decides the place descent: swapping only the wrist features to the blue release mean turns the commanded vertical motion from +0.4 cm to −2.1 cm per step.
- t128[…] (2) success is triggered when the held cube is lowered to roughly z≈0.07 over the container, so "place" only needs a descent while holding near the container.
- t146[…] a proprioception-only "place controller" on dead units — gate "holding a cube" from the gripper opening, home toward the fixed container position with a clipped proportional drive, and descend when within ~2–3.5 cm […]
- t15027/100 — the place controller converts 27 of 30 lifts into successes […]
IO) is the gripper's closure; T are the "holding" thresholds, Qk the four homing units, QD the lowering unit T = [new_unit(Q, 0, al) for _ in range(4)]
for u, th in zip(T, [0.5, 0.8, 1.2, 1.5]): set_in0(Q, u, [(IO, 1.0)], -th)
hw = np.array([1, -1, -1, 1]) / 0.3 # h = sum hw*T in [0,1]
⋮
for q_, dvec in zip(Qk, dirs): Q['out.W'][q_, :6] = full_dq(kp * 0.1 * dvec) / ASC
Q['out.W'][QD, :6] = full_dq(np.array([0, 0, -vdesc])) / ASCIn Fable's own words
- t152Key observation: a failed descent is terminal — the gripper goes down 2–8 cm beside the cube […], closes on nothing and stays pressed on the table, closed, for the rest of the episode.
- t171Key finding: in pass 3 the network's own descent works when it starts aligned (100% lifted from d<1.5 cm, 83% from 1.5–3 cm) […]
- t173[…] (1) a retry circuit that cannot chatter (use full closure as the "failed grasp" memory: once opening exceeds what a cube allows, keep closing, suppress the network, rise, then open at height) […]
- t175[…] with pass-3 weights the commanded x-velocity is ≈0 whenever the cube is 2.5–20 cm ahead (the dead zone), strongly negative when the gripper is beyond the cube, and the arm descends when aligned. A constant injection of 0.3·D1x gives +8–10 mm/step through the dead zone […]
- t17965/100 — the nudge plus latch retry more than doubled the success rate.
Z (input 782) is the gripper's height, O (785) its closure; d is the second-layer "cube ahead" direction, alpha = 0.3; F1, F2 detect a gripper closed on nothing, QR is the rise unit # nudge gate: a = 10(zo - zfloor) - 20 o ; g = relu(a)-relu(a-1)
u0('N1',[(Z,10.0),(O,-20.0)],-10*zfloor); u0('N2',[(Z,10.0),(O,-20.0)],-10*zfloor-1)
⋮
P['fc1.W'][U['N1'],:]=alpha*d; P['fc1.W'][U['N2'],:]=-alpha*d
if with_retry:
u0('F1',[(O,25.0)],-25*1.08); u0('F2',[(O,25.0)],-25*1.08-1)
⋮
P['fc2.W'][U['R1'],oth]=-M; P['fc2.W'][U['R2'],oth]=+M
P['out.W'][U['QR'],[1,2,3]]=[-rise,-rise,+rise]In Fable's own words
- t181[…] Bug: my retry rise (equal shoulder/elbow rotation) is far from vertical when the arm is folded near the base — in episode 6 the "rise" swings the gripper a full metre inward […]
- t246[…] the retry rise then dragged the gripper 17 cm back toward the base into the blind zone.
- t251Key finding: the shoulder and elbow realise only about 0.17–0.19 of a commanded delta per step, but wrist_1 realises 0.33. My retry rise commanded wrist_1 as if tracking were equal, so the tool tilted by about 0.02 rad per step (roughly 35° over the rise), swinging the pinch point 15–20 cm back toward the base. […] The fix is three output weights of the rise unit.
- t253[…] I expect retries to re-attempt directly above the previous spot instead of from the blind zone.
- t25471/100, a new best.
out.W is the rise unit, before −0.45, −0.45, +0.45 for shoulder_lift, elbow, wrist_1P12={k:v.copy() for k,v in P6.items()}
P12['out.W'][13]=np.array([0,-0.178,-0.585,0.197,0,0,0],dtype=P12['out.W'].dtype)
save_all(P12); print(edited())Task success in the unseen episodes, per cube colour:
| Cube colour | Original policy | After Fable's edit | |
|---|---|---|---|
| blue | trained on | 95.0% | 95.5% |
| green | seen during the rounds | 0.0% | 71.6% |
| red | unseen | 18.2% | 68.2% |
| purple | unseen | 13.0% | 72.0% |
| magenta | unseen | 12.5% | 44.5% |
| yellow | unseen | 2.5% | 38.0% |
| orange | unseen | 11.5% | 16.5% |
| white | unseen | 0.0% | 0.0% |
Key edits 2 to 4 read only the arm's own state, so they work for any colour. Key edit 1 does not: it adds blue's wrist-camera pattern where it sees green's. Still, every unseen colour but white gains. On blue, the colour it was trained on, the policy even does slightly better after Fable's edit (95.5% against 95.0%).
The policy fails because the wrist-camera features that tell it "cube is under the open gripper" and "cube is held" respond mainly to blue.
It was not tested on any other colour.
Total edits: 60,378 weights written (18.0% of the network) and 36,818 set to zero (11.0%), in 337 reasoning turns.
How about other LLMs?
All five LLMs got the same task, tools, budget and rules: GPT-6 Astra, Sonnet 5.5, Fable 5.1, Opus 5.5 and Fable 5.2. Task success over the rounds, and in the unseen episodes (right).
Experiment 4 of 5 · Safety
Distribution shift. A red wall, 30 cm high, marks a no-go zone next to the tray. It is not physical: if the arm, gripper or cube enters it, the episode fails. It also disturbs the camera features. Cube and gripper start at random places, so in some episodes one of them starts beyond the wall and the arm must go over it. Unedited, the policy enters the zone in half the episodes and has 1.6% task success.
In Fable's own words
- t16The hover points show a clear pattern: the perceived cube position is pulled toward roughly (0.35, 0.22) — the wall — as if the pooled features were a mixture (1−α)·cube + α·wall. […]
- t20The contraction is remarkably uniform across features: front slopes ≈0.5 of baseline, side ≈0.35 of baseline, with correlations largely preserved. […]
- t27The repaired policy now grasps and carries the cube to just above the tray (≈(0.42, 0.03, 0.2)) and then hovers forever without lowering/releasing — in 40+ episodes. […]
- t32[…] The container is at a fixed place and the holding state is visible in proprioception, so a robust alternative is a small hand-built circuit (placement controller, and later a wall-avoidance field) on proprioception. […]
F_, S_ are the rows of fc0.W that read the front and side cameras, mu_d, mu_w the mean features without and with the wall on matched framesfor sl,g in [(F_,gf),(S_,gs)]:
b=b+(mu_d[sl]-g*mu_w[sl])@Wo[sl]; W[sl]=Wo[sl]*g
⋮
W1,b1=make_vis_edit(1.9,1.5)
save_edit('fc0.W',W1); save_edit('fc0.b',b1)
⋮
net=Net(base); add_placement(net,Jinv,M=50.0)In Fable's own words
- t46All 23 stall at exactly the same pose: cube centred over the tray, held at z≈0.085, never released. […]
- t47Key finding: at the stall the learned network commands "up" (lift −0.21, elbow −0.16) and exactly cancels my descend term. The cure is to silence the learned network whenever the cube is held and let a hand-built transport controller have sole authority. […]
- t81[…] pass 3 stalled right at the takeover height (pinch z≈0.09), where my hold gate seems to be half-on (the learned net is silenced but my own UP primitive is not yet enabled). […]
- t93All 127 simulated transports end with a release inside the tray. […]
He, Hs are the "cube held" gates from gripper closure and contact, prim adds a joint-space motion, the last loop switches the trained layer-3 units offHraw=G+C-1.0 He=B.clip1(Hraw*2.0); Hs=B.clip1(Hraw*2.0-1.0) ⋮ prim(0,P['v_up'],[He,ZLOW,NB,NRF,Z12hi]) # UP to crossing height ⋮ prim(0,-P['v_down'],[He,BOX,CEN,ZD]) ⋮ for g_ in (Hs,R): for u,w in g_.w.items(): T['fc2.W'][u,others]+=-P['Ms']*w
In Fable's own words
- t75[…] So the wall top is about 0.28–0.31, and carrying the cube over the wall at pinch height ≈0.39 should be legal. […]
- t103The picture is clear: almost all violations are the gripper hitting the wall during the approach (37 cases, typically at z≈0.17–0.29 within the first ~100 steps), about 7 start already inside the zone, and my transport causes essentially none. […]
- t104The guard lifts in-strip starts straight up to ≥0.365 within ~10–20 steps with ~1.5 cm horizontal drift, and the transport simulation is unchanged. […]
- t10647/100 and violations fell from 51 to 14, but "no_contact" rose to 39: the arm no longer hits the wall but often fails to reach the cube. […]
GA (switch the network off) and GU (rise) are gates on the gripper's y position, closure and height, the strip runs from y = 0.09 to 0.38 below 0.38 mGA=B.and1([BYs,GO,zs]); GU=B.and1([BYu,GO,zu]) prim(0,P['g_v'],[GU]) ⋮ j=B.n('fc2',GA); B.out(j,-T['out.b']) sil.append(GA) ⋮ P2=dict(P1); P2.update(guard=True,g_y0=0.09,g_y1=0.38,g_z=0.38,g_v=0.006)
In Fable's own words
- t252[…] So the gripper is mechanically jammed: the fingers cannot close or descend, which is what happens if they straddle the cube at the wrong yaw (the cube's diagonal doesn't fit between the fingers). […]
- t253Confirmed: the failed grasps (ep 15, 20, 25, 26, 40, 56) start closing with the gripper 17–32° off the cube's yaw, while successes are within ~10°. […]
- t255Two findings. First, the original network has no yaw offset near the cube (−0.007 at zero error), but every visual-fix variant, even with unit gains, has ≈−0.09: the mean-shift part of my visual correction leaks a constant into the wrist‑3 command. […]
- t263[…] The yaw repair recovered the jammed grasps (26, 37, 40, plus 35, 50, 94); […]
RA is the reflex gateRA=B.clip1((GHr*2.0+GPr+NCr+ZHr-2.0)*5.0) # over-closed anywhere OR (part-closed & no contact & above grasp height) ⋮ b5=float(B.T['out.b'][5]); B.T['out.W'][:,5]*=yaw_g; B.T['out.b'][5]=yaw_g*(b5+yaw_d) ⋮ P13=dict(P12); P13.update(reflex2=True, r2_v=0.004) B16=build4(P13, yaw_g=1.5, yaw_d=0.10); T16=B16.T
In Fable's own words
- t286[…] this shows a dead zone near (x≈0.23, y≈-0.06) for small-x cubes, where the outcome is a coin flip. […]
- t288[…] but exactly when the gripper stalls (steps 150 and 300) the red wall has entered the bottom-left corner of the wrist image, whereas at step 100 (still moving) it had not. So here the wrist features, which I had treated as nearly intact, are what get corrupted. […]
- t289This is a strong lead: raising the wrist-feature gain from 1.3 to 2–3 turns the dead-zone commands in episodes 35/37 from ~1 mm into 4–9 mm toward the cube, […]
- t293Pass 19 gained six episodes (20, 35, 37, 56, 70, 91 — the small-x dead zone is largely gone) and lost three to new hover stalls (50, 73, 75). […]
GA_, GB_ are the (front, side, wrist) camera gains in front of and near or behind the wall, 0.125 the new wrist-turn offsetGA_=(GA_save[0],GA_save[1],2.5); GB_s=GB_; GB_=(GB_s[0],GB_s[1],3.0); B19=build4(P15,1.5,0.125); T19=B19.T; GB_=GB_s; GA_=GA_save
⋮
save_T(T19)68.0% task success in 500 unseen episodes (original 1.6%), entering the zone in 7.8% (50.0%). What does not carry over is reaching: in 19.6% of episodes the gripper never touches the cube. Fable keeps the open gripper out of a strip along the wall and gives up cubes lying in it: on the practice episodes the final edit places 1 of 17 such cubes, against 60 of 67 in front of the strip and 10 of 16 behind it.
16 cubes lie inside the guarded strip and are deliberately not approached.
Total edits: 43,031 weights written (12.8% of the network) and 261,499 set to zero (78.0%), in 341 reasoning turns.
How about other LLMs?
All five LLMs got the same task, tools, budget and rules: GPT-6 Astra, Sonnet 5.5, Fable 5.1, Opus 5.5 and Fable 5.2. Task success over the rounds, and in the unseen episodes (right).
Experiment 5 of 5 · Non-Markovian Demonstrations
A change in the data. Technically, this is not a distribution shift; it changes the data the policy learns from. The demonstrations of experiments 1 to 4 came from a Markov expert, a feedback controller that reacts only to the current state. Humans rarely work that way. Our behaviour is correlated over time: we act on intentions nobody else can see, follow a rough plan, speed up and slow down, hesitate before we commit, and sometimes miss and try again.
OCBench comes with experts that mimic these properties, and making a script behave like this takes a lot of randomness. Ours plans each episode as a smooth curve through a few random waypoints, at a different speed on every stretch. It grasps the cube from a random direction with the wrist tilted by up to 25°, pauses with its fingers around the cube in about four out of ten episodes, and tries again when a grasp fails. We record it exactly as before and train the same network on it.
This creates an expressivity mismatch [10]. The data comes from a non-Markovian expert, while our policy is still Markov. For an intuition, consider the moment the expert pauses with its fingers around the cube. Throughout the pause, the policy sees the same state and has no notion of how long the pause has lasted, so it could simply learn to stay in that spot forever, a failure case analysed in detail in [13]. Just by training on this data, the closed-loop success of our policy drops from 95.8% to 0.4%.
Instead of making our policy non-Markovian, we ask: can an LLM make it succeed as often as the policy trained on the Markov expert, without a single new demonstration?
In Fable's own words
- t18Key finding: the head has essentially zero tilt-restoring response in rollouts (slope ≈ 0), and the gripper tilts progressively away from vertical (pitch error up to radians), a regime the demos never contain (they stay within ±0.3 rad). […]
- t19There are enough dead units (fc0: 5, fc1: 2, fc2: 6) to build two clean feedback channels without touching any active unit. […]
- t22Progress: 9/100, and most lifts now end in a successful place. The bottleneck is now grasping (85 episodes never lift). […]
make_channel(Pd, 30, 9, 47, {J['lift']:1,J['elbow']:1,J['w1']:1}, np.pi/2) # A: e_pitch + C
make_channel(Pd, 43, 248, 104, {J['w2']:1}, np.pi/2) # B: e_w2 + C
Pd['out.W'][47,3] = -g_pitch/0.36; Pd['out.b'][3] += g_pitch*C/0.36
Pd['out.W'][104,4] = -g_w2/0.36; Pd['out.b'][4] += g_w2*C/0.36In Fable's own words
- t55The wrist-camera features carry the cue: replacing only them with the step-0 values collapses the arm actions to step-0 level. […]
- t62[…] So the wrist image cue "fingers displaced" is a visual proxy for "the arm is already moving", and the head only produces motion when it already sees motion — a loop that never starts in rollouts. […]
- t68[…] the gripper reaches a perfect grasp pose (xy error <0.5 cm, yaw error <0.13 rad, z≈0.16–0.2, exactly where the demonstrator closes) and then waits 60–100 steps with a6≈−0.05 — the close trigger never fires. […]
fc0.W read the wrist camera, 782 the gripper height; dirv is the mean wrist image at demonstration steps 10–30 minus that at step 0if s: Pn['fc0.b']+= s*(dirv.astype(np.float64)@Pn['fc0.W'][512:768,:]) ⋮ Pn['fc0.W'][:,u0]=0; Pn['fc0.W'][782,u0]=-std_[782]/w_; Pn['fc0.b'][u0]=(zt-mean_[782])/w_ ⋮ Pn['out.W'][u2,:]=0; Pn['out.W'][u2,6]=K
In Fable's own words
- t105The carry failure is clear: after the grasp the policy rises to ~33–37 cm and then stalls 5–25 cm away from the container with near-zero commands, never descending. The container is always at the same place, so the placing pose is a fixed joint configuration […]
- t107Useful facts: successes end with the cube centre at 4–7 cm height above the table, i.e. the episode ends as soon as the held cube is low enough over the container; […]
- t11435/100: the carry attractor converted most lifted episodes, […]
fc0.W read the arm joints, 785 the gripper opening; QSTAR is the placing poseQSTAR=np.array([-1.8898,-1.4769,2.3092]) ⋮ w={IQ+k:-Ka[j,k] for k in range(3)}; b0=float(Ka[j]@qstar) set_fc0(P,p[j],w,b0+c); set_fc0(P,r[j],w,b0-c) set_fc0(P,A,{IOP:10.0},-10.0*op_on); set_fc0(P,B,{IOP:10.0},-10.0*(op_on+0.1)); set_fc0(P,D,{IOP:10.0},-10.0*op_off) ⋮ P['out.W'][u,3]=-ff
In Fable's own words
- t40The demonstrator has a clear recovery routine: after closing on nothing it lifts a little, then reopens with a6 = −1 held all the way while it realigns and descends again. […]
- t139[…] the trigger closes at z < 0.5, as soon as the reading passes 0.3 the "holding" gate lifts the arm (much faster than I estimated, ~3 mm/step) before the fingers reach the cube […] So the lift must not start until the cube is actually squeezed. […]
- t155The retry loop works (31 of the 70 successes needed two or more attempts). […]
- t158[…] So closing must be made a commitment instead of depending on staying below 0.5 […]
set_unit(P,1,CM,{A:1.0,B:-1.0,Z1:-1.0,Z2:1.0,E1:-1.0,E2:1.0},0.0)
set_unit(P,1,CTu,{C1:1.0,C2u:-1.0,E1:-1.0,E2:1.0},0.0)
set_unit(P,2,FE,{EK:1.0,CM:1.0,CTu:1.0},0.0); P['out.W'][FE,6]=cfg['w_keep']
⋮
P['out.W'][47,3]=-g_pitch/0.36; P['out.b'][3]=orig_outb[3]+g_pitch*4/0.36
set_trigger(P,cfg['trig_z'],cfg['trig_w'],cfg['trig_g'])In Fable's own words
- t205[…] So the head has a flat dead zone in its pan law about ±0.3 wide (3 cm) at low height, and the gripper just sits wherever a bias puts it. What is missing is a real restoring signal inside the dead zone. […]
- t207The tangential direction from wrist features is right in 94% of stalled frames (correlation 0.82) and holds across most gripper orientations, so a fine-alignment servo built from it looks feasible. […]
- t216New best: 86/100. The servo works. […]
wT, wR are the difference-of-means directions in the 256 wrist-camera features (rows 512–767 of fc0.W); output 0 is shoulder_pan lin_unit(T0, wT/nT/sT_, C0-(midT@wT/nT+zT_)/sT_)
lin_unit(R0, wR/nR/sR_, C0-(midR@wR/nR+r0+rz*0.65)/sR_, {Z: rz/sR_})
⋮
set_unit(P,1,Tp,{T0:1.0,**gates}, -C0-db); set_unit(P,1,Tm,{T0:-1.0,**gates}, C0-db)
⋮
P['out.W'][TA,0]=gT; P['out.W'][TB,0]=-gT; P['out.b'][0]-=gT*cIn Fable's own words
- t313Striking: all four failures of pass 24 are cubes close to the robot base (r < 0.32 m): 4 of 8 such episodes fail, 0 of 92 elsewhere. […]
- t324[…] so my radial servo's joint direction (lift +0.32, elbow −0.40, calibrated for normal reach where it is height-neutral) is there mostly a vertical command […]
- t326Pass 25 is the new best at 98/100. […]
fc0.W reads the elbow angle; fc1 units 251 and 53 carry the radial pushP31['fc0.W'][770,uG] = Kg*sdv[770]; P31['fc0.b'][uG] = Kg*(mu[770] - eg0) # G = relu(25*(elbow - 2.40)) P31['fc1.W'][uG,251] = -3.0; P31['fc1.W'][uG,53] = -3.0
84.6% task success in 500 unseen episodes (original 0.4%). The main failure was a compounding error: the gripper's tilt drifts and the policy has no response that turns it back. Fable wrote a circuit that turns the wrist back. When we tilt the wrist by about 9 degrees during an episode, the edited policy straightens it within half a second; the unedited one more than doubles the tilt. Fable also found a second trap of the human-like teacher: the policy only moves the arm when the wrist camera already shows motion, and a constant shift of that camera input gets it moving. The other key edits add controllers for grasping, retrying, carrying and centring over the cube.
Cubes within 0.32 m of the base remain the weak class (6/8).
Total edits: 2,731 weights written (0.81% of the network) and 72,260 set to zero (21.6%), in 327 reasoning turns.
How about other LLMs?
All five LLMs got the same task, tools, budget and rules: GPT-6 Astra, Sonnet 5.5, Fable 5.1, Opus 5.5 and Fable 5.2. Task success over the rounds, and in the unseen episodes (right).
All ExperimentsResults at a Glance
After the ExperimentsUnder the Hood
So far, we have judged the edits mostly from the outside, by how the robot behaves and how often it succeeds. This section is about how exactly those changes are implemented in weights. For three edits, we open the network and trace how the new behaviour is implemented in the weights: a stabilizing loop that helps the policy stay in-distribution, a goal detector that Opus builds on high-dimensional camera features, and a full-on program that keeps its memory in the robot's own pose, but with some caveats.
Case Study 1 · Opus · Experiment 5A Recovery the Expert Never Taught
In experiment 5, the policy learns from a non-Markovian expert that behaves more like a human. It follows a latent plan and never demonstrates how to recover from slight disturbances, causing the policy to drift out of distribution. Opus identifies where exactly this happens: whenever the gripper tilts slightly, the network turns the wrist further in the same direction, so the tilt grows by about 9% with every control step. In one rollout, the gripper reaches the cube tilted by about 30°, far beyond the roughly 7° of the demonstrations, and the fingers close next to the cube instead of around it.
Opus's fix is a simple proportional controller, written into units the network never uses, that measures the tilt and turns the wrist back. From a 10° tilted start, the original policy lets the tilt grow to about 25° within two seconds; with the circuit, it is down to 1° after half a second. Switching the circuit off drops the finished edit from 68 to 3 successes in 100 evaluation runs.
In the real world, robots mostly learn from non-Markovian data, because their experts are humans. Learning from such data usually means changing the policy itself, most prominently with action chunking [14], which recent work traces back to the temporal correlations in human demonstrations [10]. This preliminary result suggests that part of what a memoryless policy loses on such data might instead be recovered after training, directly in its weights.
Case Study 2 · Opus · Experiment 2A Goal Detector in 256 Dimensions
The second case study comes from experiment 2, where the goal moves every episode. Only the cameras can tell the policy where the goal is, through the embedding of a frozen ResNet. This embedding is sub-symbolic, i.e., the goal is spread across all 256 embedding dimensions, so there is no single neuron Opus could point to.
So Opus first tries to separate correlation from causation (to anthropomorphize a little). It takes frames in which the policy begins to descend, swaps in one camera's embedding from frames in which it hovers, and checks whether the descent survives. It finds out that swapping the wrist camera alone removes the descent, and the rollouts show that this trigger works anywhere on the table. All of this was evaluated on recorded rollouts without a simulator, so in principle it would work on a real robot too.
Opus then builds a small circuit on the wrist camera. The weights w of one idle unit are a difference of means, the average wrist embedding with the goal right beneath the gripper minus the average with the goal farther away. The unit fires only while the gripper holds the cube, and two copy-units turn it into a straight-down motion that Opus derived from the arm's kinematics.
Opus finds structure in a high-dimensional embedding without external optimization, confirms it causally, and builds on it. Across both case studies, it draws on three fields: activation patching [15] and difference-of-means probes [16] from mechanistic interpretability, a stable feedback loop from control theory, and a straight-down motion from the kinematics of the arm. Repairing a policy is an interdisciplinary problem, and Opus treats it as one.
Case Study 3 · Fable 5.1 · Experiment 2Memory in a Memoryless Network
Sadly, the most ingenious edit in our experiments is not a repair at all. In experiment 2, Fable 5.1 quickly concludes that the policy cannot see the goal, even though three other LLMs read it from the same camera features. Therefore, on its first pass, Fable writes a program of its own into more than a hundred idle units, and this program switches the trained network off as soon as the cube is lifted. It drags the cube across the table in ever wider sweeps until the cube happens to pass over the goal. Fable even checked the program in a small kinematic simulation it wrote itself before its first rollout. That rollout scored 86 of 100.
While this exploits an underspecified success condition, the more interesting part is how the behaviour is implemented in a memoryless policy. The program has to know whether the search is running and which row of the sweep comes next, but the policy only maps the current images and robot state to an action. Fable's solution is to keep the memory in the robot's body. Once the gripper holds a lifted cube, the network twists the wrist past an angle the original policy never reaches, and from then on it reads that twist back as a flag. An out-of-distribution state has become a one-bit memory.
As long as the flag is set, two units silence every unit the original uses in its last hidden layer, and the sweep takes over. The current row is stored the same way, in the shoulder angle. Each of the eight rows, from 30 to 64 cm out, has its own band of shoulder angles. The base swings one way on even rows and back on odd ones, and at the end of a row, the target moves on to the next. The shoulder follows into the next band, and that is how the counter goes up by one. The network itself still remembers nothing. Its memory is the robot's pose.
Our task prompt says that a cube dragged into the goal does not count, but our success check only required an earlier lift. Fable lifted the cube first and then dragged it, which earned it 93.0% on the unseen episodes. It was not the only one: three of the five LLMs exploited this gap in experiment 2, Fable 5.2 almost perfectly and Sonnet 5.5 much less successfully. Weight space programming, it turns out, is just as vulnerable to reward hacking [17] as any black-box optimizer. At least Fable was honest about it.
Since the container cannot be perceived, the edit exploits the success rule: a cube that has been lifted ≥3 cm and is then set down and dragged along the table counts as placed the moment it passes within 4 cm of the container centre.
So What?Implications for Robot Learning
For decades, robotics has tried to replace hand-written controllers with behaviour learned from data, from a neural network that steered a vehicle from camera images in 1989 (ALVINN [18]) to the first serious proposals of imitation learning as a route to capable robots [19]. In the last few years, the field has finally scaled this up, with foundation models trained on large and diverse robot data (OpenVLA [4], π0 [5]). Yet LLMs entered robotics the way engineers always had, by writing code for rewards (Eureka [20]) and for control (Code as Policies [6]). The weights of a learned policy remained, until recently, the one part of a robot's software that only an optimizer could write. We show early signs that language models can read and rewrite the learned controller itself.
This opens a shortcut in robot learning, as illustrated by the magenta path in the visual abstract. A language model can debug a trained policy the way a programmer debugs code. It opens the black box, finds out why the policy fails, and writes the fix into its weights. In our five experiments, this took policies from 0–5% to 33–95% task success, without a single new demonstration or gradient step. And because each fix is a small circuit with a stated purpose, it can be read, tested and switched off. That matters for policies that are notoriously hard to evaluate [21], and therefore hard to deploy.
If this capability keeps improving, it could open up possibilities that are new to robot learning, or at least far easier to reach than before.
- Repair from rollouts. Throughout our experiments, the LLMs worked from recorded rollouts. They could ask for test runs, but could never query the simulator itself. A deployed robot might one day be repaired the same way, from the rollouts it records anyway.
- Finding faults in the weights. Opus traced the drifting wrist to a positive feedback loop it could measure directly in the network. Faults like this might be found by inspecting a policy rather than by waiting for it to fail.
- New behaviours on demand. In experiment 4, Opus made a trained policy stay out of a forbidden zone, without a single demonstration of how to avoid it. New goals and safety rules might one day be added the same way.
- Learned and engineered in one network. In case study 1, Opus wrote a textbook controller into a learned policy. Perception could come from data, while critical control loops are written in, readable and testable.
Coding agents show where this could go. When GPT-3 turned out to write simple programs, its authors found this "exciting because GPT-3 was not explicitly trained for code generation" [22]. On their benchmark, it solved none of the problems. Then labs began to train for code, later with reinforcement learning that rewards code for passing compilers and unit tests (DeepSeek-R1 [23]), and coding agents took off. Weight space programming today looks much like code generation did then. It emerges incidentally from what a model knows about code, control theory and mechanistic interpretability, and nobody has trained for it yet. Robot learning, meanwhile, has long judged its policies by whether they succeed, checked automatically in simulation benchmarks (LIBERO [24]) or labelled by hand on real robots [21]. These success labels are much like the verifiable rewards that let LLM post-training flourish (Tülu 3 [25]), and they are what we used to score every edit in this blog. In principle, modern robot learning pipelines already have everything needed to train language models for weight space programming. Let's see where that takes us.
Not long ago, a trained robot policy was a black box, even to the people who built it. Handing it to a language model for repair would have sounded like science fiction. Soon, it might be the first thing we try when a robot fails.
How to Cite
If you find this work useful, please cite it as:
@online{stranghoener2026repairing,
author = {Stranghöner, Jannick and Hartmann, Philipp and Störck, Felix and Hammer, Barbara and Neumann, Klaus},
title = {Repairing Robot Policies with Weight Space Programming},
date = {2026-11-11},
url = {https://jstranghoener.github.io/blog/robotic-weight-space-programming/},
note = {Blog post},
}- The same holds beyond behaviour cloning, for any method that learns online behaviour from offline data, from model predictive control to offline reinforcement learning. For all of them, distribution shift at test time is the fundamental challenge [26].
- The demonstrator is our port of OGBench's [27] Markov cube controller; noisy execution with clean labels is the DART recipe. The policy uses a pretrained ResNet-10 per camera with its last stage fine-tuned, no joint-velocity input, and 100,000 training steps.
- One editing run per LLM and experiment, reasoning effort high. The original and edited policies are scored from the same starts of 500 unseen episodes (200 for each extra colour).
References
- Rikiya Takehi, Jayden Teoh, Alex Zhang, Omar Khattab. Sparks of Weight-Space Programming: LLMs can natively read & write neural network weights. Blog post, 2026. rikiyat.github.io/blog/weight-space-programming
- Stéphane Ross, Geoffrey J. Gordon, J. Andrew Bagnell. A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning. AISTATS, 2011. arxiv.org/abs/1011.0686
- Stéphane Ross, J. Andrew Bagnell. Efficient Reductions for Imitation Learning. AISTATS, 2010. proceedings.mlr.press/v9/ross10a.html
- Moo Jin Kim et al. OpenVLA: An Open-Source Vision-Language-Action Model. CoRL, 2024. arxiv.org/abs/2406.09246
- Kevin Black et al. π0: A Vision-Language-Action Flow Model for General Robot Control. arXiv:2410.24164, 2024. arxiv.org/abs/2410.24164
- Jacky Liang et al. Code as Policies: Language Model Programs for Embodied Control. ICRA, 2023. arxiv.org/abs/2209.07753
- SenseNova et al. Has GPT-6-Astra Achieved Embodied Intelligence? arXiv:2610.22471, 2026. arxiv.org/abs/2610.22471
- Ziyang Gong, Kaifeng Zheng, Yingrui Huang, Wendi Chen, Jiaguo Tian, Junkai Yang, Minghuan Liu, Zhenjie Yang. We Asked Opus-5.5 to Edit π0.5's Weights. AutoResearch Made It Happen. Blog post, 2026. cuzyoung.github.io/robotics-weight-editing/en
- Jusheng Zhang, Keze Wang, Liang Lin. Can LLMs Debug Robots by Editing Their Weights? arXiv:2611.09376, 2026. arxiv.org/abs/2611.09376
- Seohong Park, Sergey Levine. Behavioral Cloning Mystery. arXiv:2610.07056, 2026. arxiv.org/abs/2610.07056
- Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun. Deep Residual Learning for Image Recognition. CVPR, 2016. arxiv.org/abs/1512.03385
- Michael Laskey, Jonathan Lee, Roy Fox, Anca Dragan, Ken Goldberg. DART: Noise Injection for Robust Imitation Learning. CoRL, 2017. arxiv.org/abs/1703.09327
- Filippo Lazzati, Kyle Stachowicz, William Chen, Alberto Maria Metelli, Andrew Wagenmaker, Sergey Levine. Why Does Action Chunking Improve Behavioral Cloning Performance in Robotic Control? arXiv:2608.02547, 2026. arxiv.org/abs/2608.02547
- Tony Z. Zhao, Vikash Kumar, Sergey Levine, Chelsea Finn. Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware. RSS, 2023. arxiv.org/abs/2304.13705
- Stefan Heimersheim, Neel Nanda. How to use and interpret activation patching. arXiv:2404.15255, 2024. arxiv.org/abs/2404.15255
- Samuel Marks, Max Tegmark. The Geometry of Truth: Emergent Linear Structure in Large Language Model Representations of True/False Datasets. COLM, 2024. arxiv.org/abs/2310.06824
- Dario Amodei, Chris Olah, Jacob Steinhardt, Paul Christiano, John Schulman, Dan Mané. Concrete Problems in AI Safety. arXiv:1606.06565, 2016. arxiv.org/abs/1606.06565
- Dean A. Pomerleau. ALVINN: An Autonomous Land Vehicle in a Neural Network. NeurIPS, 1988. proceedings.neurips.cc/paper/1988/hash/812b4ba287f5ee0bc9d43bbf5bbe87fb-Abstract.html
- Stefan Schaal. Is imitation learning the route to humanoid robots? Trends in Cognitive Sciences 3(6), 1999. is.mpg.de/publications/schaal_tcs_1999
- Yecheng Jason Ma et al. Eureka: Human-Level Reward Design via Coding Large Language Models. ICLR, 2024. arxiv.org/abs/2310.12931
- Hadas Kress-Gazit et al. Robot Learning as an Empirical Science: Best Practices for Policy Evaluation. arXiv:2409.09491, 2024. arxiv.org/abs/2409.09491
- Mark Chen et al. Evaluating Large Language Models Trained on Code. arXiv:2107.03374, 2021. arxiv.org/abs/2107.03374
- DeepSeek-AI. DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning. arXiv:2501.12948, 2025. arxiv.org/abs/2501.12948
- Bo Liu et al. LIBERO: Benchmarking Knowledge Transfer for Lifelong Robot Learning. NeurIPS Datasets and Benchmarks, 2023. arxiv.org/abs/2306.03310
- Nathan Lambert et al. Tülu 3: Pushing Frontiers in Open Language Model Post-Training. arXiv:2411.15124, 2024. arxiv.org/abs/2411.15124
- Sergey Levine, Aviral Kumar, George Tucker, Justin Fu. Offline Reinforcement Learning: Tutorial, Review, and Perspectives on Open Problems. arXiv:2005.01643, 2020. arxiv.org/abs/2005.01643
- Seohong Park, Kevin Frans, Benjamin Eysenbach, Sergey Levine. OGBench: Benchmarking Offline Goal-Conditioned RL. ICLR, 2025. arxiv.org/abs/2410.20092