fix(control): a command's reply wait ends at its deadline while events keep arriving #538

Open
jarynclouatre wants to merge 1 commit from jarynclouatre/Longshot:fix/ctl-reply-deadline into main
Contributor

Type: fix · Risk: low · Tested on: Linux x86-64 (game-free checks only) · Needs: none

Why

Bridge.request (scripts/longshot_ctl.py) computes one deadline for a command's reply, but without reply_timeout it handed line() the whole wait again after every line that was not the reply. A client subscribed to present or log events, whose command then gets no reply, waited as long as the events kept coming: with an event every 5 s, a status that should give up after 70 s never did.

None filed.

How to reproduce

Seen on: found reading the code; not seen in the game
Settings and switches: A client subscribed to present or log events, without reply_timeout.
Steps: Send status, receive an event every 5 s, and give the command no reply.
Expected: The command gives up after 70 s.
Actual: The command waits as long as events keep coming, so it never gives up while they continue.

What changed

A command now keeps one deadline while events arrive without a reply. Each read gets only the time left, while buffered replies are still read.

  • control client (scripts/longshot_ctl.py): each read gets the time left to the one deadline. Once it has passed and no line is buffered, the request fails with the timeout's code 3 and "no answer from the game in s", the whole wait. A buffered line, the reply among them, is still read. quit's closed connection and reply_timeout are unchanged.
  • tests (tests/test_longshot_ctl.py): a fake bridge on a controlled clock (no real waiting): events every 5 s and no reply fail with code 3 by 70 s; a reply at 69 s after events is taken; with reply_timeout=3 it still ends by 3 s. The existing wait test runs on a stopped clock, so it still sees each command's whole wait (3610, 70, 15).

Measurements

Verdict: the new checks fail on main and pass here; the suite has no new failures.

Hardware: none used (game-free checks on a Linux x86-64 PC, Python 3.14)
Scene: none, the game was not run
Method: 2026-11-14. python3 -I -B tests/run_all.py, two runs each, interleaved (main, PR, main, PR). The new case also ran with main's scripts/longshot_ctl.py. Branch: fix/ctl-reply-deadline.
How to reproduce: the commands above in a checkout of each commit

Before: main @ 1be60eae After: this PR @ a847cd0f
tests/run_all.py (run 1, run 2) 485 passed, 2 failed, 84 skipped (both) 485 passed, 2 failed, 84 skipped (both)
failing checks test_bake_portability.py, test_walk_key.py the same two
tests/test_longshot_ctl.py, main's client fails: still waiting at 265 s (the events ended at 195 s) passes

test_bake_portability.py fails here for a missing Python module (zstandard). test_walk_key.py's fixture doesn't link on main (undefined reference to hos_oot_autoload_input).

Flags and settings

None.

Testing

Tested: the game-free suite on a Linux PC, as in Measurements.

Not tested: a client against a running game.

Must be tested: None.

Risks

What could break: #56 and #494 change other lines of longshot_ctl.py.

Turning it off: Reverting the commit brings the renewed wait back.

Compatibility: A command whose reply comes after its wait while events keep arriving now fails where it waited before; the waits themselves are unchanged (60 s plus 10, an hour for inputs, or the bridge's timeout plus 10). watch reads events with line() itself and is unchanged.

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how.
  • Rebased on current main.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named).
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
**Type:** fix · **Risk:** low · **Tested on:** Linux x86-64 (game-free checks only) · **Needs:** none ## Why `Bridge.request` (`scripts/longshot_ctl.py`) computes one deadline for a command's reply, but without `reply_timeout` it handed `line()` the whole wait again after every line that was not the reply. A client subscribed to present or log events, whose command then gets no reply, waited as long as the events kept coming: with an event every 5 s, a `status` that should give up after 70 s never did. None filed. ## How to reproduce **Seen on:** found reading the code; not seen in the game **Settings and switches:** A client subscribed to present or log events, without `reply_timeout`. **Steps:** Send `status`, receive an event every 5 s, and give the command no reply. **Expected:** The command gives up after 70 s. **Actual:** The command waits as long as events keep coming, so it never gives up while they continue. ## What changed A command now keeps one deadline while events arrive without a reply. Each read gets only the time left, while buffered replies are still read. - control client (`scripts/longshot_ctl.py`): each read gets the time left to the one deadline. Once it has passed and no line is buffered, the request fails with the timeout's code 3 and "no answer from the game in <n> s", the whole wait. A buffered line, the reply among them, is still read. `quit`'s closed connection and `reply_timeout` are unchanged. - tests (`tests/test_longshot_ctl.py`): a fake bridge on a controlled clock (no real waiting): events every 5 s and no reply fail with code 3 by 70 s; a reply at 69 s after events is taken; with `reply_timeout=3` it still ends by 3 s. The existing wait test runs on a stopped clock, so it still sees each command's whole wait (3610, 70, 15). ## Measurements **Verdict:** the new checks fail on main and pass here; the suite has no new failures. Hardware: none used (game-free checks on a Linux x86-64 PC, Python 3.14) Scene: none, the game was not run Method: 2026-11-14. `python3 -I -B tests/run_all.py`, two runs each, interleaved (main, PR, main, PR). The new case also ran with main's `scripts/longshot_ctl.py`. Branch: `fix/ctl-reply-deadline`. How to reproduce: the commands above in a checkout of each commit | | Before: main @ 1be60eae | After: this PR @ a847cd0f | |---|---|---| | `tests/run_all.py` (run 1, run 2) | 485 passed, 2 failed, 84 skipped (both) | 485 passed, 2 failed, 84 skipped (both) | | failing checks | `test_bake_portability.py`, `test_walk_key.py` | the same two | | `tests/test_longshot_ctl.py`, main's client | fails: still waiting at 265 s (the events ended at 195 s) | passes | `test_bake_portability.py` fails here for a missing Python module (zstandard). `test_walk_key.py`'s fixture doesn't link on main (`undefined reference to hos_oot_autoload_input`). ### Flags and settings None. ## Testing **Tested:** the game-free suite on a Linux PC, as in Measurements. **Not tested:** a client against a running game. **Must be tested:** None. ## Risks **What could break:** #56 and #494 change other lines of `longshot_ctl.py`. **Turning it off:** Reverting the commit brings the renewed wait back. **Compatibility:** A command whose reply comes after its wait while events keep arriving now fails where it waited before; the waits themselves are unchanged (60 s plus 10, an hour for inputs, or the bridge's timeout plus 10). `watch` reads events with `line()` itself and is unchanged. ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
fix(control): a command's reply wait ends at its deadline while events keep arriving
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 6s
beanstalk/approval Test farm not requested
Website / Website on Cloudflare (pull_request_target) Has been skipped
a847cd0fd0
Bridge.request computed one deadline, but without reply_timeout it
handed line() the whole wait again after every line that was not the
reply. A client subscribed to present or log events whose command got
no reply waited for as long as the events came.

Each read now gets the time left to the one deadline, and the request
fails with the timeout's code 3 once it has passed and no line is
buffered.
jarynclouatre changed title from WIP: fix(control): a command's reply wait ends at its deadline while events keep arriving to fix(control): a command's reply wait ends at its deadline while events keep arriving 2026-11-14 18:45:30 +00:00
First-time contributor

Independent, targeted validation of your existing PR538 head a847cd0fd05e482b0266eb07224f581ef9ab79cf, compared with pinned pre-fix main 7ff5256e000671d0fccf27f77ba41275916b21df. The anonymous main/PR refresh at 2026-11-14 19:05 UTC still showed those commits and an open, unmerged PR.

I replayed your exact two public method bodies (test_input_waits_as_long_as_the_bridge and test_reply_wait_ends_at_its_deadline_while_events_arrive) in a fresh unittest.TestCase, using the complete unchanged client source for each revision. This deliberately bypassed LongshotCtl.setUpClass; it was not native full-module or full-suite execution.

Two interleaved pairs (main, head, main, head) agreed:

  • The public long-input control retained [3610, 70, 15] on both.
  • On main, the public event-budget method raised code 3 but failed at 265 <= 70: the finite event stream ended at virtual 195 s and another 70 s was granted. That failure stops the method before its later reply/explicit-timeout subcases.
  • On your head, the complete selected public methods passed, including the reply at 69 s and explicit 3 s budget.

Additional private controls exercised the real Bridge.request/send/line, with scheduled fake byte transport rather than overriding line: an already-buffered matching reply at a zero deadline; an event, unrelated reply and matching reply coalesced exactly at 70 s (with the trailing event retained); real-line no-reply timeout at 70; a reply at 71 after an event at 69 rejected at 70; explicit 3 s timeout on both revisions; and status/quit EOF behavior. All six private scenarios passed on head, twice. Main reproduced the two real-line deadline failures; buffering and EOF compatibility controls also passed on main.

Eight unique methods/scenarios, 32 method executions across four isolated Python processes: 26 passes and six expected baseline assertion failures, zero harness errors. Native fixture exits were 1, 0, 1, 0; complete receipts and byte/AST fingerprints are retained locally for review. All times above are virtual, not latency measurements. No production or public-test changes were made, no competing fix was started, and no real sockets, product processes, Windows pipe path, game, or full suite were exercised. This is scoped evidence, not full-PR approval.

Assisted-by: Hermes Agent (OpenAI GPT-6.1-sol)
Co-Authored-By: Claudia (OpenAI GPT-6.1-sol)

Independent, targeted validation of your existing PR538 head `a847cd0fd05e482b0266eb07224f581ef9ab79cf`, compared with pinned pre-fix main `7ff5256e000671d0fccf27f77ba41275916b21df`. The anonymous main/PR refresh at 2026-11-14 19:05 UTC still showed those commits and an open, unmerged PR. I replayed your exact two public method bodies (`test_input_waits_as_long_as_the_bridge` and `test_reply_wait_ends_at_its_deadline_while_events_arrive`) in a fresh unittest.TestCase, using the complete unchanged client source for each revision. This deliberately bypassed `LongshotCtl.setUpClass`; it was **not** native full-module or full-suite execution. Two interleaved pairs (main, head, main, head) agreed: - The public long-input control retained `[3610, 70, 15]` on both. - On main, the public event-budget method raised code 3 but failed at `265 <= 70`: the finite event stream ended at virtual 195 s and another 70 s was granted. That failure stops the method before its later reply/explicit-timeout subcases. - On your head, the complete selected public methods passed, including the reply at 69 s and explicit 3 s budget. Additional private controls exercised the **real Bridge.request/send/line**, with scheduled fake byte transport rather than overriding line: an already-buffered matching reply at a zero deadline; an event, unrelated reply and matching reply coalesced exactly at 70 s (with the trailing event retained); real-line no-reply timeout at 70; a reply at 71 after an event at 69 rejected at 70; explicit 3 s timeout on both revisions; and status/quit EOF behavior. All six private scenarios passed on head, twice. Main reproduced the two real-line deadline failures; buffering and EOF compatibility controls also passed on main. Eight unique methods/scenarios, 32 method executions across four isolated Python processes: 26 passes and six expected baseline assertion failures, zero harness errors. Native fixture exits were `1, 0, 1, 0`; complete receipts and byte/AST fingerprints are retained locally for review. All times above are virtual, not latency measurements. No production or public-test changes were made, no competing fix was started, and no real sockets, product processes, Windows pipe path, game, or full suite were exercised. This is scoped evidence, not full-PR approval. Assisted-by: Hermes Agent (OpenAI GPT-6.1-sol) Co-Authored-By: Claudia (OpenAI GPT-6.1-sol)
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 6s
beanstalk/approval Test farm not requested
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/ctl-reply-deadline:jarynclouatre-fix/ctl-reply-deadline'
git switch 'jarynclouatre-fix/ctl-reply-deadline'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!538
No description provided.