Skip to content

feat(engine): resume a crashed flatten without sending a second close - #488

Merged
MarcosACH merged 9 commits into
mainfrom
ralph/issue-474
Oct 12, 2026
Merged

MarcosACH merged 9 commits into
mainfrom
ralph/issue-474

Conversation

@MarcosACH

Copy link
Copy Markdown
Owner

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

  • Each close takes the next __operator__ seq. The seq high-water fold moved from StrategyHost to Cache.seq_high_water(), so strategies and flatten start from the same number.
  • Before it closes a symbol, flatten waits for the verdict on any __operator__ close the boot left open in that symbol. Then it reads the size again, because the old close may have filled.
  • If that close is still open after the wait, flatten sends nothing for the symbol. The final read reports its size, and the next boot resumes the saga. So a symbol never has two closes open.
  • The wait runs after the replay has played, because its reconcile passes move the replay clock.

Tests

tests/engine/test_flatten_e2e.py covers paper with the in-memory bus:

  • A second flatten uses a seq above every earlier one.
  • A crash after the send and before the booking: the next flatten books the fill by split, closes the rest, and exits 0.
  • A close the boot left open makes flatten wait. If it is still open, flatten sends nothing.
  • cancel-all after 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

MarcosACH and others added 4 commits October 12, 2026 11:13
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>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 14:48
@MarcosACH MarcosACH added the ralph:needs-review PR opened, awaiting code-review label Oct 12, 2026
@MarcosACH MarcosACH self-assigned this Oct 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

… 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 MarcosACH left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_cancel sets no cancel_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

@MarcosACH MarcosACH added ralph:changes-requested Code-review found BLOCKING/WARN issues; must fix and removed ralph:needs-review PR opened, awaiting code-review labels Oct 12, 2026
MarcosACH and others added 4 commits October 12, 2026 11:58
…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>
@MarcosACH

Copy link
Copy Markdown
Owner Author

Review fixes for a5b92b7. Each new test was checked against a mutation of _resume_closes. Each one went red, then green on the real code.

  • R001 Status: resolved — 8fb8dd5. test_a_failed_read_after_the_wait_stops_flatten_with_no_second_close fails the position read after the wait. It asserts exit 1, left=read_failed: ..., and no seq 2 close.
  • R002 Status: resolved — 205c94b, b961e9d. test_a_close_left_open_that_fills_during_the_wait_leaves_only_the_rest_to_close fills the old close after flatten's first read. The next close must be 0.01 and FILLED. b961e9d fixes a mypy error in that test.
  • R003 Status: resolved — f27904d. seq_high_water folds over self.all_orders() again. ADR-0016, ADR-0024, and the docstring all say the fold reads it, so I kept the method instead of deleting it.

Gate: ruff, mypy, lint-imports pass. 2022 passed, coverage 97.18%.

@MarcosACH MarcosACH added ralph:review-addressed Fixes pushed; awaiting re-review and removed ralph:changes-requested Code-review found BLOCKING/WARN issues; must fix labels Oct 12, 2026

@MarcosACH MarcosACH left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@MarcosACH MarcosACH added ralph:ready Clean review; eligible for merge and removed ralph:review-addressed Fixes pushed; awaiting re-review labels Oct 12, 2026
@MarcosACH
MarcosACH merged commit 4c1be11 into main Oct 12, 2026
11 checks passed
@MarcosACH
MarcosACH deleted the ralph/issue-474 branch October 12, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ralph:ready Clean review; eligible for merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resume a flatten that crashed mid-run without closing twice

2 participants