dita_v2: unknown != flat (VenuePostAckError) + lossless telemetry lane

BUG CLASS (doc: BUGCLASS_INDETERMINATE_OUTCOME_20260713.md): an operation with an external
side effect has THREE outcomes — NOT_ATTEMPTED, ATTEMPTED_REFUSED, ATTEMPTED_INDETERMINATE
— and rollback is sound only for the first two. Collapsing the third into 'failed' is what
orphaned 6 live SHORTs: a post-ack TypeError reached rust_backend's 'except Exception ->
synthetic REJECTED -> FSM rollback', which asserted 'no order exists' about an order that
was already filled. Telemetry never had a veto; it hijacked the failure channel.

FIXES (no new seams, no re-architecture):
- venue.py: VenuePostAckError — typed channel meaning THE EFFECT EXISTS. Carries receipt.
- bingx_venue submit/submit_async: point-of-no-return fence. Post-ack bookkeeping failures
  raise VenuePostAckError instead of a bare exception.
- rust_backend (BOTH submit paths): catch VenuePostAckError FIRST -> no synthetic REJECT,
  no rollback. Slot stays working; E-feed FULL_FILL / reconcile settles the truth.

LOSSLESS TELEMETRY (HJ: 'DITAv2 exists precisely because seams dropped 40% of inputs'):
drop-oldest is data loss and is GONE. Exec path appends O(1) to an unbounded queue and
returns. A SEPARATE spiller thread (which never touches the plane, so a wedged plane cannot
starve it) parks the backlog above HWM into a durable append-only spool; the publisher
replays the spool when the plane recovers.
Proven: wedged-forever plane + 200k records -> 0 dropped, 195903 durable on disk, 4096 in
memory, 7.4 us/call on the exec path. Lossless AND memory-bounded. Healthy plane: 2000/2000.

STILL BROKEN, flagged to codex: the pre-ack branch rolls back on TIMEOUT — but a timeout is
the definition of INDETERMINATE (the order may have filled). Same bug class, older, live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-07-13 10:31:39 +02:00
parent b13b54e769
commit 3d75e4b59d
4 changed files with 405 additions and 40 deletions

View File

@@ -19,6 +19,27 @@ from .contracts import (
from .exchange_event import ExchangeEvent
class VenuePostAckError(Exception):
"""Raised when submit fails AFTER the venue has already accepted the order.
THE POINT OF NO RETURN. A plain exception out of submit()/submit_async() is
ambiguous: it may mean "the venue never got the order" (safe to roll the FSM
back to IDLE) or "the venue filled it and a line below blew up" (rolling back
is catastrophic — the venue keeps the position while the kernel believes it is
flat). On 2026-07-13 a post-ack TypeError took the first interpretation and
orphaned 6 live SHORTs.
Post-ack failure is NOT failure. It is UNKNOWN — and unknown is never flat.
Callers MUST NOT synthesise a REJECTED event for this error: leave the slot in
its working state and let the E-feed FILL / reconcile settle the truth.
"""
def __init__(self, message: str, *, receipt: Any = None, events: Optional[List[VenueEvent]] = None):
super().__init__(message)
self.receipt = receipt
self.events = events or []
class VenueAdapter(Protocol):
"""Abstract venue adapter used by the kernel."""