Repository navigation
emrg: a boss call that times out still locks the owner out for four hours #2499
Description
A boss call that times out still locks the owner out for four hours
What is observed, and where
emrg/server/daemon.py, in _run_boss_child (~line 8137): the owner clock is stamped before the owner's turn runs.
child.set_boss_asked_at(asked_at)That position was chosen deliberately — a turn that dies mid-way must leave the clock written, so the next call does not pay twice for the same judgement. But the stamp is now written on a path that delivers nothing: _run_boss_tool (~line 8070) cancels the owner's turn when asyncio.wait(..., timeout=timeout_s) expires and returns
{"error": "boss: the owner did not answer within …s, so its turn was
cancelled. Nothing from it was delivered. … Raise `timeout_s` if the
owner needs longer to read this session's history."}
The clock is already written by then, so the very next call — including the retry with a larger timeout_s that this error message tells the caller to make — returns the busy notice instead of starting a turn, for REASK_WINDOW_S (four hours).
Why that is a defect and not a cost
The four-hour window exists because a direction that was just given stands: a second call inside it re-buys a judgement that cannot have moved. A timed-out call gave no direction at all — it is the one outcome after which the judgement has not been bought yet. So the window is applied exactly where its own justification is absent, and the failure is not the delay alone:
- The error's own remedy is unusable. It says to raise
timeout_s; raising it cannot help, because no turn starts. - The busy notice says "carry on with the plan you already have, or finish and report" — true for the intended case, false here: the round asked for direction, was refused, and is told to proceed as if it had one.
- It is sticky. The clock lives in the owner session's
meta.json, so it survives a daemon restart (that is its purpose), and nothing in the tool can clear it: there is no argument, no command, and no reader that reports "a call timed out and nothing was delivered" as distinct from "a stance was given four minutes ago".
Measured on this checkout: tests/test_boss_tool.py has no test that calls twice across a timeout — the six window tests cover the answered case, the rebuilt session and the turn that raised, but not the turn that ran out of time.
What is expected instead
A call that delivered nothing must not consume the window.
| Situation | Behaviour |
|---|---|
| The owner answered | the clock stands; the window applies |
| The owner's turn raised | the clock stands (a died turn must not be re-bought) |
The owner's turn was cancelled by timeout_s |
the clock must not stand — the next call starts a turn |
| A busy notice | says no direction was decided (already true) |
| The timeout error message | must not advise a retry the window refuses |
What the fix must satisfy
- The retry after a timeout starts a turn. The decisive reading is a second call after a timed-out first one that observes a turn was started — not one that reads a flag.
- The died-turn case keeps its guarantee. A turn that raises still leaves the clock written; the existing test for that must stay green, so the two paths are told apart rather than the stamp moved wholesale after the turn.
- The distinction is in the clock, not only in the message. A fix that rewrites the timeout text but leaves the stamp is not a fix: the window is what refuses the retry.
- No new state that a restart loses. If the fix needs "the last call delivered nothing", it belongs beside
boss_asked_atinmeta.json, with the same treatment of a malformed value thatboss_asked_atgets. - A repository test asserts the retry-after-timeout path, and a test asserts the died-turn path still holds the clock.
Not claimed
The four-hour figure itself is not in question here, and this issue proposes no change to it. The macOS case is not claimed: Test runs ubuntu-latest and windows-2025, and the logic is platform-free Python.
Activity
how2how2how2-arch commentedon Oct 22, 2026
Handled by #2502