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
The harness decides when the policy stack runs, and holds the schedule the stack produced. Both belong to the
stack, and the two together cap what a serving algorithm can express.
Where this starts
Harness._step returns before entering the stack whenever a call is outstanding:
So for the duration of a model call, no wrapper is entered at all. Two shipped wrappers need to be:
TemporalStack fills its window by recording one sample per call. Across an inference gap it records
nothing, and _StackBuffer.sample carries the last value forward — so a video-conditioned model trained on
continuous frames is served a frozen one for the length of the call.
This one is worth stating as more than a race, because the arm is built to make it happen. franka.py
clears its own errors — the control loop calls recover_from_errors() on every iteration it is in error, at
a 2 kHz rate limit — and says so:
ifin_error:
# The driver always clears a recoverable error itself; making it optional (hold in# ERROR for out-of-band recovery instead) is a config knob to add when an embodiment# needs it.robot.recover_from_errors()
So ERROR is transient by construction, and a fault can appear and clear well inside one inference window.
The harness looks at the arm only when it builds an observation, which it does not do while a call is
outstanding, so the whole episode goes unseen: no stop, no re-plan, and _play carries on driving waypoints
planned for a pose the arm has since left. Polling for a state the driver is actively erasing is the wrong
shape — the fault is an edge, and it needs latching rather than sampling. Before playing moved into the
harness the driver owned the trajectory and saw every state at its own rate, so it cancelled on a fault of
any length.
Neither is a regression: before #591 the call blocked the loop thread, so both were starved identically. What
changed is that the rig now keeps moving through the gap instead of freezing in it, which makes the gap matter.
What we want to be able to write
A developer should be able to express any reasonable serving algorithm as a wrapper, with codecs and wrappers
as the tools. Concretely: real-time chunking and temporal ensembling
(Zhao et al. 2023, Black et al. 2025),
which re-query before the current chunk is exhausted and merge successive predictions rather than replacing
them.
Most of that already works. #591 gives re-query-before-exhaustion — the harness keeps playing while a call is
in flight, which _ReplanEarly in test_harness.py pins. A wrapper that wants to merge rather than replace can
hold its own chunks and return the merged whole. The gaps are the observation hole above, and that only one call
can be in flight.
The principle: the policy owns its schedule. It decides when to infer, what to release, and when. For that
to be writable rather than guessed at, the rig has to commit to what it provides, and to what it will do
regardless.
Goal 1 — the stack observes every tick
One sentence, two victims: the harness must enter the stack on every control tick, including while a call is
outstanding, so a wrapper that needs to watch can watch.
Two candidate shapes:
AsyncInfer as a wrapper. Move the "a call is in flight" gate out of the harness and into the stack, as a
position in the chain: layers outside it run every tick, layers inside run on the worker. The harness then
enters the stack every tick and is told None. Needs no protocol or wire-format change — the harness already
does executor.submit(session, obs); this does it one layer in. It does mean async_infer joins WIRE_WRAPPERS, so the server declares whether the rig overlaps inference with execution. That is defensible:
overlap is a property of the serving algorithm (RTC requires it, plain chunked scheduling does not), and the
server is the party that knows the model's latency and horizon.
An observe split. The harness calls observe(obs) down the chain every tick and __call__ only at
re-query. Threading stays where it is; nothing joins the wire vocabulary. The cost is a second method every
wrapper must keep consistent with the first.
For the fault half specifically, what is delivered has to be real before delivery is fixed: today the harness
computes the fault (_is_faulted, stapled on as keys.ROBOT_FAULT) because the serializer drops a faulted arm's
frame. #637 makes robot_state.status robot data the serializer emits on every frame, which also closes #619.
No edge or latch is needed on top: on the Franka a fault stays in ERROR for seconds, so once the stack is
entered every tick every poll sees it.
Goal 2 — the contract between rig and algorithm
If the server declares the whole algorithm, local half included, then the rig owes it a description of the
ground it stands on.
What the rig guarantees the algorithm:
Emission is execution. A returned action goes out at the instant it was read. Nothing is buffered between
the harness and the device — _cancel_session already relies on this ("devices hold their last commanded
position; nothing is buffered downstream to clear").
Last command wins, immediately. No queueing, interpolation, or reduction in the driver.
Timing bounds. Never called earlier than it asked for; a stated worst-case lateness; and a guaranteed
maximum interval regardless, so a wrapper cannot wedge the rig by asking badly.
One timeline.now() and obs[keys.OBS_TIME_NS] are the same clock, and an observation is no older than a
stated bound when the call is made.
Lifecycle. Reset before an episode ends, and no calls after.
Each of these stands on its own and none needs the above.
Drop timestamps from what the session returns. Today the session returns a trajectory of timestamped
actions and the harness holds it in a TrajectoryPlayer until each waypoint is due. Instead let the session
return the command to execute now, and make releasing the wrapper's own business — which is what "the policy
owns its schedule" means concretely. ChunkedSchedule holds its chunk and releases from it; the harness keeps
start, stop and the emitters. _install, _play, the players and the timestamp on the wire between stack and
harness all go, and RTC/TE become wrappers manipulating a plan they own rather than merging against a buffer
someone else holds.
The constraint that decides the design: _pace sleeps to the next waypoint precisely because the harness holds
the schedule —
due= [tsforplayerinself._players.values() if (ts:=player.next_due()) isnotNone]
returnpimm.Sleep(min(POLL_PERIOD_SEC, max(min(due) -clock.now_ns(), 1) /1e9))
Move the schedule into the stack and the harness has nothing to pace on; it polls, and every waypoint lands up
to a poll period late. Sim is unaffected (fixed control period, still deterministic); a rig loses
waypoint-accurate timing, which is most of what the per-tick wire buys. So this needs the session to say when it
next has something to do — the same value TrajectoryPlayer.next_due() already is, sourced from the session and
translated by _pace into the pimm.Sleep it already returns.
Delete cancel() as a protocol. Every wrapper implements it and must remember super().cancel();
forgetting is silent. It exists only to return a session to its start, and the system already knows how to build
a fresh one. Reset becomes discard-and-rebuild of the inner session.
Tighten the return contract. A scheduling wrapper currently normalizes sloppy server output:
A single-action session may return a bare dict, and a no-codec path may omit timestamp (servers can
stamp/truncate themselves); normalize both so an immediate action executes instead of raising.
That belongs once at the wire boundary. Left where it is, every future wrapper pays the same tax.
Collapse the two classes per wrapper.StopOnFault is roughly twenty lines of scaffolding — wrapper, _Session, wrap_session, to_spec — around four lines of behaviour. The split is needed (one policy serves
many robots; sessions are per-episode), but the ceremony could reduce to a base taking just the behaviour.
Two more, lower value: now threads through all fifteen wrap_session signatures for one consumer, though it
likely survives — ChunkedSchedule anchors to inference completion, which no observation stamp supplies, and
a callable cannot live in context. And to_spec spells each wire name a second time; deriving it from WIRE_WRAPPERS would couple the table back to the classes it is deliberately decoupled from.
Explored and parked
Recorded so they are not re-derived, and not mistaken for requirements.
Sessions as pimm control systems. Buys nothing. harness.py is the only file in positronic/policy that
imports pimm, and it exists to be exactly that adapter.
An observe/deliver fold (observations pull down one leg, commands push up the other, terminating in the
emitters). Solves "release a command without an observation arriving", which is not a problem we have — every
tick carries an observation. It costs the codec correlation: Codec.decode(data, context=obs) is free today
because encode and decode happen in one call, and splitting the legs makes twelve stateless codecs stateful
and forces request/response tagging.
Wrappers as generators. Genuinely clearer for the two or three real state machines — _trajectory_end
stops being a field and becomes where the generator is suspended, and reset is discard-and-rebuild. But it is
an internal implementation style behind __call__, not a change to any contract, and pool.submit(inner, obs)
already delegates a call to a worker without it. Adoptable per wrapper, later, on its own merits.
A next_due that lets the harness skip building observations. Defeated by its own premise: StopOnFault
sits outermost in every shipped pipeline (cfg/wrappers.video_context_wrappers is StopOnFault() | stack | ChunkedSchedule()) and always answers "next tick", so the value never survives to
the top. next_due remains useful for pacing (above), not for skipping work.
A safety layer the harness enforces regardless of the declared stack — stopping the arm on a fault
without StopOnFault, refusing an observation past a staleness bound. Declined. The declared stack is the
mechanism, and StopOnFault is how the arm stops: a server that omits it has declared a stack that does not
stop, and the harness will not second-guess that. The harness's job on a fault is delivery — the stack must
see every fault, including one that starts and clears inside an inference window (Goal 1) — not action.
The trial deadline is unaffected: it is the trial's budget, not the algorithm's, and the harness already
ends the trial on it.
The harness decides when the policy stack runs, and holds the schedule the stack produced. Both belong to the
stack, and the two together cap what a serving algorithm can express.
Where this starts
Harness._stepreturns before entering the stack whenever a call is outstanding:So for the duration of a model call, no wrapper is entered at all. Two shipped wrappers need to be:
TemporalStackfills its window by recording one sample per call. Across an inference gap it recordsnothing, and
_StackBuffer.samplecarries the last value forward — so a video-conditioned model trained oncontinuous frames is served a frozen one for the length of the call.
StopOnFaultreadskeys.ROBOT_FAULToff an observation. During a call no observation is built, so a faultthat starts and clears inside the inference window is never seen, and the pre-fault chunk keeps playing
(Play the policy's trajectory in the harness, one command per channel per round #591, thread on
harness.py).This one is worth stating as more than a race, because the arm is built to make it happen.
franka.pyclears its own errors — the control loop calls
recover_from_errors()on every iteration it is in error, ata 2 kHz rate limit — and says so:
So
ERRORis transient by construction, and a fault can appear and clear well inside one inference window.The harness looks at the arm only when it builds an observation, which it does not do while a call is
outstanding, so the whole episode goes unseen: no stop, no re-plan, and
_playcarries on driving waypointsplanned for a pose the arm has since left. Polling for a state the driver is actively erasing is the wrong
shape — the fault is an edge, and it needs latching rather than sampling. Before playing moved into the
harness the driver owned the trajectory and saw every state at its own rate, so it cancelled on a fault of
any length.
Neither is a regression: before #591 the call blocked the loop thread, so both were starved identically. What
changed is that the rig now keeps moving through the gap instead of freezing in it, which makes the gap matter.
What we want to be able to write
A developer should be able to express any reasonable serving algorithm as a wrapper, with codecs and wrappers
as the tools. Concretely: real-time chunking and temporal ensembling
(Zhao et al. 2023, Black et al. 2025),
which re-query before the current chunk is exhausted and merge successive predictions rather than replacing
them.
Most of that already works. #591 gives re-query-before-exhaustion — the harness keeps playing while a call is
in flight, which
_ReplanEarlyintest_harness.pypins. A wrapper that wants to merge rather than replace canhold its own chunks and return the merged whole. The gaps are the observation hole above, and that only one call
can be in flight.
The principle: the policy owns its schedule. It decides when to infer, what to release, and when. For that
to be writable rather than guessed at, the rig has to commit to what it provides, and to what it will do
regardless.
Goal 1 — the stack observes every tick
One sentence, two victims: the harness must enter the stack on every control tick, including while a call is
outstanding, so a wrapper that needs to watch can watch.
Two candidate shapes:
AsyncInferas a wrapper. Move the "a call is in flight" gate out of the harness and into the stack, as aposition in the chain: layers outside it run every tick, layers inside run on the worker. The harness then
enters the stack every tick and is told
None. Needs no protocol or wire-format change — the harness alreadydoes
executor.submit(session, obs); this does it one layer in. It does meanasync_inferjoinsWIRE_WRAPPERS, so the server declares whether the rig overlaps inference with execution. That is defensible:overlap is a property of the serving algorithm (RTC requires it, plain chunked scheduling does not), and the
server is the party that knows the model's latency and horizon.
observesplit. The harness callsobserve(obs)down the chain every tick and__call__only atre-query. Threading stays where it is; nothing joins the wire vocabulary. The cost is a second method every
wrapper must keep consistent with the first.
For the fault half specifically, what is delivered has to be real before delivery is fixed: today the harness
computes the fault (
_is_faulted, stapled on askeys.ROBOT_FAULT) because the serializer drops a faulted arm'sframe. #637 makes
robot_state.statusrobot data the serializer emits on every frame, which also closes #619.No edge or latch is needed on top: on the Franka a fault stays in
ERRORfor seconds, so once the stack isentered every tick every poll sees it.
Goal 2 — the contract between rig and algorithm
If the server declares the whole algorithm, local half included, then the rig owes it a description of the
ground it stands on.
What the rig guarantees the algorithm:
the harness and the device —
_cancel_sessionalready relies on this ("devices hold their last commandedposition; nothing is buffered downstream to clear").
maximum interval regardless, so a wrapper cannot wedge the rig by asking badly.
now()andobs[keys.OBS_TIME_NS]are the same clock, and an observation is no older than astated bound when the call is made.
Goal 3 — simplify wrappers (optional, independent)
Each of these stands on its own and none needs the above.
Drop timestamps from what the session returns. Today the session returns a trajectory of timestamped
actions and the harness holds it in a
TrajectoryPlayeruntil each waypoint is due. Instead let the sessionreturn the command to execute now, and make releasing the wrapper's own business — which is what "the policy
owns its schedule" means concretely.
ChunkedScheduleholds its chunk and releases from it; the harness keepsstart, stop and the emitters.
_install,_play, the players and the timestamp on the wire between stack andharness all go, and RTC/TE become wrappers manipulating a plan they own rather than merging against a buffer
someone else holds.
The constraint that decides the design:
_pacesleeps to the next waypoint precisely because the harness holdsthe schedule —
Move the schedule into the stack and the harness has nothing to pace on; it polls, and every waypoint lands up
to a poll period late. Sim is unaffected (fixed control period, still deterministic); a rig loses
waypoint-accurate timing, which is most of what the per-tick wire buys. So this needs the session to say when it
next has something to do — the same value
TrajectoryPlayer.next_due()already is, sourced from the session andtranslated by
_paceinto thepimm.Sleepit already returns.Delete
cancel()as a protocol. Every wrapper implements it and must remembersuper().cancel();forgetting is silent. It exists only to return a session to its start, and the system already knows how to build
a fresh one. Reset becomes discard-and-rebuild of the inner session.
Tighten the return contract. A scheduling wrapper currently normalizes sloppy server output:
That belongs once at the wire boundary. Left where it is, every future wrapper pays the same tax.
Collapse the two classes per wrapper.
StopOnFaultis roughly twenty lines of scaffolding — wrapper,_Session,wrap_session,to_spec— around four lines of behaviour. The split is needed (one policy servesmany robots; sessions are per-episode), but the ceremony could reduce to a base taking just the behaviour.
Two more, lower value:
nowthreads through all fifteenwrap_sessionsignatures for one consumer, though itlikely survives —
ChunkedScheduleanchors to inference completion, which no observation stamp supplies, anda callable cannot live in
context. Andto_specspells each wire name a second time; deriving it fromWIRE_WRAPPERSwould couple the table back to the classes it is deliberately decoupled from.Explored and parked
Recorded so they are not re-derived, and not mistaken for requirements.
harness.pyis the only file inpositronic/policythatimports pimm, and it exists to be exactly that adapter.
emitters). Solves "release a command without an observation arriving", which is not a problem we have — every
tick carries an observation. It costs the codec correlation:
Codec.decode(data, context=obs)is free todaybecause encode and decode happen in one call, and splitting the legs makes twelve stateless codecs stateful
and forces request/response tagging.
_trajectory_endstops being a field and becomes where the generator is suspended, and reset is discard-and-rebuild. But it is
an internal implementation style behind
__call__, not a change to any contract, andpool.submit(inner, obs)already delegates a call to a worker without it. Adoptable per wrapper, later, on its own merits.
next_duethat lets the harness skip building observations. Defeated by its own premise:StopOnFaultsits outermost in every shipped pipeline (
cfg/wrappers.video_context_wrappersisStopOnFault() | stack | ChunkedSchedule()) and always answers "next tick", so the value never survives tothe top.
next_dueremains useful for pacing (above), not for skipping work.without
StopOnFault, refusing an observation past a staleness bound. Declined. The declared stack is themechanism, and
StopOnFaultis how the arm stops: a server that omits it has declared a stack that does notstop, and the harness will not second-guess that. The harness's job on a fault is delivery — the stack must
see every fault, including one that starts and clears inside an inference window (Goal 1) — not action.
The trial deadline is unaffected: it is the trial's budget, not the algorithm's, and the harness already
ends the trial on it.