MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3771

Planning bug: a card proposed 'restore the yield, not the stepping' — the two are not separable, and the record it cited already said so

Done
Description

A planning defect, not a product one. The correction is already applied — plan cmtcxffdu00b9hwn8ftdnxx0l, submitted 2026-08-28 — and this card is the record of what the planner got wrong.

What was planned

MOTIR-3763 asked the container supervisors to yield between polls with ctx.step.sleep, sized at 5 points / 60 minutes, on the argument that

"step.sleep did two jobs — survive the platform ceiling AND release the slot — and only the first was a platform artifact. What is restored is the yield alone, not the stepping."

Its parent story MOTIR-3758 wrote the same claim into its acceptance criterion 2, and the decision card MOTIR-3761 — authored by the same planning pass — wrote it into job-queue-foundation.md §15.4 as a recorded decision, with an explicit cost argument ("linear rather than N², ~366 memo lookups") that counted only the memoized steps.

Why it is false

lib/jobs/engine/runner.ts's runQueuedJob re-invokes the handler from the top after a yield, and only step.run results are memoized. So every UN-memoized call before a yield re-executes on every later pass. In a supervision loop that call is the POLL, which is a real orchestrator round-trip.

Probe, 2026-08-28, through the real createStepApi against the real job_step table, driving the proposed shape:

4 sleeps  ->  PASSES=5  POLLS=10  STEPS=4

N(N+1)/2. At ~122 polls per 30-minute index: 7 503 orchestrator reads instead of 122.

Two more consequences the plan did not see: a JobStepYield is a throw, so runIndexContainer's teardown finally would have destroyed the container on the first poll; and §15.4a's amendment to §13.1 limb 3 was written on the same false premise.

What the planner should have done

The fact that falsifies it is stated, in bold, in the very record the card cites. job-queue-foundation.md §13.2 reads: "A reclaim re-invokes the handler FROM THE TOP … Each one is a JobStepYield, a re-enqueue, a re-claim by some worker and a replay of the handler from the top, during which every earlier step is re-read from job_step. A loop that polls N times therefore performs on the order of N² memo lookups."

The pass read that paragraph — it quotes the N² figure — and applied it only to the MEMO lookups, because those are what the sentence enumerates. The un-memoized work between the yields is not in that sentence, and it is the expensive half. The pass then wrote its own arithmetic ("three memo rows per replay, so linear") which is arithmetic over the wrong quantity.

The check that would have caught it costs one probe and no reasoning: run the proposed shape and count the side effects.

Still open, and deliberately not settled here

Whether supervision should become a self-rescheduling state machine. The re-plan files it as its own story with the measurement that would schedule it; nothing in this bug decides it.

Evidence

  • Probe measured on branch parent/MOTIR-3758-worker-fairness, base 13a30285f (origin/main), local Postgres 15.18.
  • lib/jobs/engine/step.tssleep() memoizes only a deadline; run() memoizes a result; nothing else is memoized.
  • docs/decisions/job-queue-foundation.md §13.2 — the paragraph that already said so.