Repository navigation
feat(engine): resume a crashed flatten without sending a second close - #488
Conversation
A second flatten on the same store reused seq 1. The manager drops a signal id it has seen, so the close never went out and the run exited 1. The counter now starts from the saga high-water fold, which moved to Cache so StrategyHost and Flatten share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A flatten dies after its close reaches the venue and before the fill is booked. The next flatten's boot books that fill by the stored split. Then it closes the other symbol with a higher seq and exits 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es again A close the venue acked and then dropped can still be open after a crash. It may have filled, so a second close beside it could close the size twice. Flatten now waits for its verdict and reads the size again. If the wait ends with the close still open, it sends nothing for that symbol. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The resume belongs to the boot, not to the flatten job. So a cancel-all run after a crashed flatten books the close's fill by its split too. The crash setup moved into helpers both tests share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fold CONTEXT.md listed what flatten had built so far, but not the resume after a crash. The Cache docstring said every exit job starts a seq counter there. Cancel-all uses no seq, so only Flatten does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MarcosACH
left a comment
There was a problem hiding this comment.
Review of ralph/issue-474 @ a5b92b7
Reviewer: code-review skill
Reviewed: 2026-10-12
Optimization axis: correctness
Mechanical
- ruff format: pass
- ruff check: pass
- mypy: pass
- lint-imports: pass (7 kept, 0 broken)
- pytest: pass (2020 passed, 49 skipped, coverage gate met)
Findings
[BLOCKING] R001 — src/tickwright/engine/exit_run.py:150 — No test covers a failed position read after the wait
Category: testing
Why: Lines 151 and 178 are the only new lines with no coverage. This branch stops the whole run and skips every symbol after it, and nothing proves it.
Fix: Add an e2e test. Leave a close open, let the wait decide it, then fail the next fetch_positions. Assert exit 1, left starts with read_failed, and no __operator__ seq 2 is placed.
Status: open
[WARN] R002 — src/tickwright/engine/exit_run.py:176 — Nothing proves the size is read again after the wait
Category: testing
Why: The re-read exists because the old close may have filled during the wait. If line 179 returned size instead, every test would stay green, since the ghost test keeps the same size.
Fix: Add a test where the close left open fills (fully or partly) during the wait. Assert the next close is sized to what the venue holds after that fill.
Status: open
[WARN] R003 — src/tickwright/engine/cache.py:63 — Cache.all_orders lost its only caller, and its docstring is now wrong
Category: docs
Why: The docstring says it exists for the seq high-water fold. The fold now reads self._orders directly, and nothing in src/ calls all_orders any more.
Fix: Remove all_orders and its tests in tests/engine/test_cache.py. If you keep it, make seq_high_water read through it, or fix the docstring.
Status: open
Notes (no action)
- ADR-0056 resume order holds. The boot resolves the open saga by cloid, then the job runs from the top. The cancel-all test proves the boot does it for any exit run.
- Seq high-water for
__operator__comes from the same fold as strategies.mark_operator_cancelsets nocancel_signal_id, so operator cancels never move a strategy's seq. - Kafka seq reuse (#442) stays out of scope, as the PRD says.
Summary
- BLOCKING: 1 open
- WARN: 2 open
- NIT: 0 open
- Merge: BLOCKED
Ralph-State: changes-requested
…o close Review R001 on #488. The branch had no test. A mutation that sized the close from the old read went green before this test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review R002 on #488. No test failed if the close kept the size from the read before the wait. Now the old close fills during the wait, and the next close must ask only for what is left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205c94b used one name for the venue order and for the stored saga. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review R003 on #488. ADR-0016, ADR-0024, and the all_orders docstring say the fold reads Cache.all_orders(). Moving the fold into the Cache made it read the dict directly, so all_orders lost its only caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review fixes for a5b92b7. Each new test was checked against a mutation of
Gate: ruff, mypy, lint-imports pass. 2022 passed, coverage 97.18%. |
MarcosACH
left a comment
There was a problem hiding this comment.
Review of ralph/issue-474 @ f27904d
Reviewer: code-review skill
Reviewed: 2026-10-12
Optimization axis: correctness
Mechanical
- ruff format: pass
- ruff check: pass
- mypy: pass
- lint-imports: pass (7 kept, 0 broken)
- pytest: pass (2022 passed, 49 skipped, coverage 97.18%)
Findings
[BLOCKING] R001 — src/tickwright/engine/exit_run.py:150 — No test covers a failed position read after the wait
Category: testing
Status: resolved — 8fb8dd5 (verified)
The test fails the second positions read. The ghost wait decides the old close first, so that read is the re-read, not the final read. If the failure were ignored, the final read would report BTC=0.02 and the test would go red.
[WARN] R002 — src/tickwright/engine/exit_run.py:176 — Nothing proves the size is read again after the wait
Category: testing
Status: resolved — 205c94b, b961e9d (verified)
I checked this with a mutation in a detached worktree. I made _resume_closes return size instead of reading again. Both new tests went red. They pass on the real code.
[WARN] R003 — src/tickwright/engine/cache.py:63 — Cache.all_orders lost its only caller, and its docstring is now wrong
Category: docs
Status: resolved — f27904d (verified)
seq_high_water now reads through all_orders(). The docstring and ADR-0016 match the code again.
New findings
None. The fix commits touch only the fold and the tests. _left_open now stores split_basis as the manager does at place (ADR-0054), so the fill can be booked.
Summary
- BLOCKING: 0 open
- WARN: 0 open
- NIT: 0 open
- Merge: READY
Ralph-State: ready
Closes #474
A crash mid-flatten must still book every fill and never send a second close for the same size. Before this change, every close used seq 1. So a second flatten on the same store sent no close for a symbol that an earlier flatten had closed. It exited 1.
What changed
__operator__seq. The seq high-water fold moved fromStrategyHosttoCache.seq_high_water(), so strategies and flatten start from the same number.__operator__close the boot left open in that symbol. Then it reads the size again, because the old close may have filled.Tests
tests/engine/test_flatten_e2e.pycovers paper with the in-memory bus:cancel-allafter a crashed flatten books the open close at boot.Every test builds
AppConfig. The full suite, ruff, mypy, and lint-imports pass.Follow-up
Flatten now keeps its own seq counter beside
SignalEmitter. #473 moves it onto the emitter, so a fix for #442 covers flatten too.🤖 Generated with Claude Code